Merge branch 'upstream'

This commit is contained in:
Andrew A. Vasilyev 2024-02-22 17:31:51 +03:00
commit bf5c737c8f
41 changed files with 1705 additions and 548 deletions

View File

@ -1,12 +0,0 @@
[workspace]
exclude = [ "build", "common-src", "perl", "scripts" ]
members = [
"pve-rs",
"pmg-rs",
]
[patch.crates-io]
# proxmox-tfa requires -time and -uuid as well, so enable *all* or *none* of them
#proxmox-tfa = { path = "../proxmox/proxmox-tfa" }
#proxmox-time = { path = "../proxmox/proxmox-time" }
#proxmox-uuid = { path = "../proxmox/proxmox-uuid" }

101
Makefile
View File

@ -1,33 +1,11 @@
CARGO ?= cargo
define to_upper
$(shell echo "$(1)" | tr '[:lower:]' '[:upper:]')
endef
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline
DEBUG_LIBPATH :=
else
DEBUG_LIBPATH := "-L./target/debug",
endif
define package_template
sed -r \
-e 's/\{\{PRODUCT\}\}/$(1)/g;' \
-e 's/\{\{LIBRARY\}\}/$(2)/g;' \
-e 's|\{\{DEBUG_LIBPATH\}\}|$(DEBUG_LIBPATH)|g;' \
Proxmox/Lib/template.pm \
>Proxmox/Lib/$(1).pm
endef
define upload_template
cd build; \
dcmd --deb lib$(1)-rs-perl*.changes \
| grep -v '.changes$$' \
| tar -cf "$@.tar" -T-; \
cat "$@.tar" | ssh -X repoman@repo.proxmox.com upload --product $(2) --dist bullseye
endef
.PHONY: all
all:
ifeq ($(BUILD_TARGET), pve)
@ -40,74 +18,29 @@ else
@echo " - make pmg"
endif
.PHONY: pve pmg
pve pmg:
@PERLMOD_PRODUCT=$(call to_upper,$@) \
RUSTFLAGS="-L/usr/lib64/perl5/CORE -lperl" $(CARGO) build $(CARGO_BUILD_ARGS) -p $@-rs
.PHONY: gen
gen:
$(call package_template,PMG,pmg_rs)
$(call package_template,PVE,pve_rs)
perl ./scripts/genpackage.pl Common \
Proxmox::RS::APT::Repositories \
Proxmox::RS::CalendarEvent \
Proxmox::RS::Subscription
perl ./scripts/genpackage.pl PVE \
PVE::RS::APT::Repositories \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
perl ./scripts/genpackage.pl PMG \
PMG::RS::APT::Repositories \
PMG::RS::Acme \
PMG::RS::CSR \
PMG::RS::OpenId \
PMG::RS::TFA
build:
rm -rf build
mkdir build
echo system >build/rust-toolchain
cp -a ./scripts ./build
cp -a ./Cargo.toml ./build
cp -a ./common ./build
cp -a ./pve-rs ./build
cp -a ./pmg-rs ./build
cp -a ./Proxmox ./build
$(MAKE) BUILD_MODE=release -C build -f ../Makefile gen
mkdir -p ./build/pve-rs/Proxmox/Lib
mv ./build/Proxmox/Lib/PVE.pm ./build/pve-rs/Proxmox/Lib/PVE.pm
mkdir -p ./build/pmg-rs/Proxmox/Lib
mv ./build/Proxmox/Lib/PMG.pm ./build/pmg-rs/Proxmox/Lib/PMG.pm
mv ./build/PVE ./build/pve-rs
mv ./build/PMG ./build/pmg-rs
mv ./build/Proxmox ./build/common/pkg
# Replace the symlinks with copies of the common code in pve/pmg:
cd build; for i in pve pmg; do \
rm ./$$i-rs/common ; \
mkdir ./$$i-rs/common ; \
cp -R ./common/src ./$$i-rs/common/src ; \
done
# So the common packages end up in ./build, rather than ./build/common
mv ./build/common/pkg ./build/common-pkg
pve-deb: build
cd ./build/pve-rs && dpkg-buildpackage -b -uc -us
touch $@
pmg-deb: build
cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
touch $@
common-deb: build
cd ./build/common-pkg && dpkg-buildpackage -b -uc -us
touch $@
pve-upload: pve-deb
$(call upload_template,pve,pve)
pmg-upload: pmg-deb
$(call upload_template,pmg,pmg)
# need to put into variable to ensure comma isn't interpreted as param separator on call
common_target=pve,pmg
common-upload: common-deb
$(call upload_template,proxmox,$(common_target))
.PHONY: clean
clean:
cargo clean
rm -rf ./build ./PVE ./PMG ./pve-deb ./pmg-deb ./common-deb
# Copy the workspace root into the sources
mkdir build/pve-rs/.workspace
cp -t build/pve-rs/.workspace Cargo.toml
sed -i -e '/\[package\]/a\workspace = ".workspace"' build/pve-rs/Cargo.toml
# Clear the member array and replace it with ".."
sed -i -e '/^members = \[/,/^]$$/d' build/pve-rs/.workspace/Cargo.toml
sed -i -e '/^\[workspace\]/a\members = [ ".." ]' build/pve-rs/.workspace/Cargo.toml
# Copy the cargo config
mkdir build/pve-rs/.cargo
cp -t build/pve-rs/.cargo .cargo/config

View File

@ -1,68 +0,0 @@
package Proxmox::Lib::{{PRODUCT}};
=head1 NAME
Proxmox::Lib::{{PRODUCT}} - base module for {{PRODUCT}} rust bindings
=head1 SYNOPSIS
package {{PRODUCT}}::RS::SomeBindings;
use base 'Proxmox::Lib::{{PRODUCT}}';
BEGIN { __PACKAGE__->bootstrap(); }
1;
=head1 DESCRIPTION
This is the base module of all {{PRODUCT}} bindings.
Its job is to ensure the 'lib{{LIBRARY}}.so' library is loaded and provide a 'bootstrap' class
method to load the actual code.
=cut
use DynaLoader;
sub library {
return '{{LIBRARY}}';
}
sub load : prototype($) {
my ($pkg) = @_;
my $mod_name = $pkg->library();
my @dirs = (map "-L$_/auto", @INC);
my $mod_file = DynaLoader::dl_findfile({{DEBUG_LIBPATH}}@dirs, $mod_name);
die "failed to locate shared library for $mod_name (lib${mod_name}.so)\n" if !$mod_file;
my $lib = DynaLoader::dl_load_file($mod_file)
or die "failed to load library '$mod_file'\n";
my $data = ($::{'proxmox-rs-library'} //= {});
$data->{$mod_name} = $lib;
$data->{-current} //= $lib;
$data->{-package} //= $pkg;
}
sub bootstrap {
my ($pkg) = @_;
my $mod_name = $pkg->library();
my $bootstrap_name = 'boot_' . ($pkg =~ s/::/__/gr);
my $lib = $::{'proxmox-rs-library'}
or die "rust library not available for '{PRODUCT}'\n";
$lib = $lib->{$mod_name};
my $sym = DynaLoader::dl_find_symbol($lib, $bootstrap_name);
die "failed to locate '$bootstrap_name'\n" if !defined $sym;
my $boot = DynaLoader::dl_install_xsub($bootstrap_name, $sym, "src/FIXME.rs");
$boot->();
}
BEGIN { __PACKAGE__->load(); }
1;

View File

@ -1,4 +1,4 @@
include /usr/share/dpkg/default.mk
include /usr/share/dpkg/pkg-info.mk
PACKAGE=libproxmox-rs-perl
@ -8,22 +8,63 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb
DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR=
all:
PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=- \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::Common \
--lib-prefix=Proxmox
# Point to any generated pm file (Proxmox/ dir is already present in this package)
Proxmox/RS/CalendarEvent.pm:
$(PERLMOD_GENPACKAGE) \
Proxmox::RS::APT::Repositories \
Proxmox::RS::CalendarEvent \
Proxmox::RS::Notify \
Proxmox::RS::Subscription
all: Proxmox/RS/CalendarEvent.pm
true
.PHONY: install
install:
install: Proxmox/RS/CalendarEvent.pm
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
find PVE \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
find Proxmox \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
rm $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/Proxmox/Lib/template.pm'
$(BUILDDIR): debian PVE Proxmox Makefile
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
cp -t $(BUILDDIR).tmp -a debian PVE Proxmox Makefile
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb
deb: $(DEB)
$(DEB): build
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS)
$(DEB): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEB)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)
.PHONY: upload
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEB)
# check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com upload --product pve,pmg --dist $(DEB_DISTRIBUTION)
clean:
rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes
rm -rf $(PACKAGE)-[0-9]*/

View File

@ -0,0 +1,100 @@
package Proxmox::Lib::SslProbe;
use strict;
use warnings;
=head1 Environment Variable Safety
Perl's handling of environment variables was completely messed up until v5.38.
Using `setenv` such as use din the `openssl-probe` crate would cause it to
crash later on, therefore we provide a perl-version of env var probing instead,
and override the crate with one that doesn't replace the variables if they are
already set correctly.
=cut
BEGIN {
# Copied from openssl-probe
my @cert_dirs = (
"/var/ssl",
"/usr/share/ssl",
"/usr/local/ssl",
"/usr/local/openssl",
"/usr/local/etc/openssl",
"/usr/local/share",
"/usr/lib/ssl",
"/usr/ssl",
"/etc/openssl",
"/etc/pki/ca-trust/extracted/pem",
"/etc/pki/tls",
"/etc/ssl",
"/etc/certs",
"/opt/etc/ssl",
"/data/data/com.termux/files/usr/etc/tls",
"/boot/system/data/ssl",
);
# Copied from openssl-probe
my @cert_file_names = (
"cert.pem",
"certs.pem",
"ca-bundle.pem",
"cacert.pem",
"ca-certificates.crt",
"certs/ca-certificates.crt",
"certs/ca-root-nss.crt",
"certs/ca-bundle.crt",
"CARootCertificates.pem",
"tls-ca-bundle.pem",
);
my $probed_ssl_vars = 0;
# The algorithm here is taken from the `openssl-probe` crate and should
# produce the exact same result in order to ensure the rust code does not
# call `setenv()`.
my sub probe_ssl_vars : prototype() {
return if $probed_ssl_vars;
$probed_ssl_vars = 1;
my $result_file = $ENV{SSL_CERT_FILE};
my $result_file_changed = 0;
my $result_dir = $ENV{SSL_CERT_DIR};
my $result_dir_changed = 0;
for my $certs_dir (@cert_dirs) {
if (!defined($result_file)) {
for my $file (@cert_file_names) {
my $path = "$certs_dir/$file";
if (-e $path) {
$result_file = $path;
$result_file_changed = 1;
last;
}
}
}
if (!defined($result_dir)) {
for my $file (@cert_file_names) {
my $path = "$certs_dir/certs";
if (-d $path) {
$result_dir = $path;
$result_dir_changed = 1;
last;
}
}
}
last if defined($result_file) && defined($result_dir);
}
if ($result_file_changed && defined($result_file)) {
$ENV{SSL_CERT_FILE} = $result_file;
}
if ($result_dir_changed && defined($result_dir)) {
$ENV{SSL_CERT_DIR} = $result_dir;
}
}
probe_ssl_vars();
}
1;

View File

@ -1,3 +1,27 @@
libproxmox-rs-perl (0.3.3) bookworm; urgency=medium
* move ssl var probing to Proxmox::Lib::SslProbe
-- Proxmox Support Team <support@proxmox.com> Thu, 07 Dec 2023 09:57:33 +0100
libproxmox-rs-perl (0.3.2) bookworm; urgency=medium
* add Proxmox::Lib::Common::probe_ssl_vars() helper
-- Proxmox Support Team <support@proxmox.com> Tue, 05 Dec 2023 10:46:39 +0100
libproxmox-rs-perl (0.3.1) bookworm; urgency=medium
* add Proxmox::RS::Notify module
-- Proxmox Support Team <support@proxmox.com> Mon, 24 Jul 2023 14:02:17 +0200
libproxmox-rs-perl (0.3.0) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Wed, 17 May 2023 15:48:41 +0200
libproxmox-rs-perl (0.2.1) bullseye; urgency=medium
* update to proxmox-subscription 0.3 / proxmox-http 0.7

View File

@ -1 +0,0 @@
12

View File

@ -1,10 +1,9 @@
Source: libproxmox-rs-perl
Section: perl
Priority: optional
Build-Depends:
debhelper (>= 12),
Build-Depends: debhelper-compat (= 13), perlmod-bin,
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.5.1
Standards-Version: 4.6.2
Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
Homepage: https://www.proxmox.com
@ -14,15 +13,12 @@ Package: libproxmox-rs-perl
Architecture: any
# always bump both versioned Depends and Breaks, otherwise systems with both
# libpmg-rs-perl and libpve-rs-perl might load an outdated lib and break
Depends:
${misc:Depends},
${perl:Depends},
${shlibs:Depends},
libpve-rs-perl (>= 0.7.2) | libpmg-rs-perl (>= 0.6.2),
Breaks:
libpve-rs-perl (<< 0.7.2),
libpmg-rs-perl (<< 0.6.2),
Replaces: libpve-rs-perl (<< 0.6.0)
Description: PVE/PMG common parts which have been ported to Rust - Perl packages
Contains the perl side of modules provided by the libraries of both libpve-rs-perl and
libpmg-rs-perl, loading whichever is available.
Depends: libpve-rs-perl (>= 0.8.5) | libpmg-rs-perl (>= 0.6.2),
${misc:Depends},
${perl:Depends},
${shlibs:Depends},
Breaks: libpmg-rs-perl (<< 0.6.2), libpve-rs-perl (<< 0.7.2),
Replaces: libpve-rs-perl (<< 0.6.0),
Description: PVE/PMG common perl parts for Rust perlmod bindings
Contains the perl side of modules provided by the libraries of both
libpve-rs-perl and libpmg-rs-perl, loading whichever is available.

14
common/src/logger.rs Normal file
View File

@ -0,0 +1,14 @@
use env_logger::{Builder, Env};
use std::io::Write;
/// Initialize logging. Should only be called once
pub fn init(env_var_name: &str, default_log_level: &str) {
if let Err(e) = Builder::from_env(Env::new().filter_or(env_var_name, default_log_level))
.format(|buf, record| writeln!(buf, "{}: {}", record.level(), record.args()))
.write_style(env_logger::WriteStyle::Never)
.format_timestamp(None)
.try_init()
{
eprintln!("could not set up env_logger: {e}");
}
}

View File

@ -1,3 +1,5 @@
pub mod apt;
mod calendar_event;
pub mod logger;
pub mod notify;
mod subscription;

508
common/src/notify.rs Normal file
View File

@ -0,0 +1,508 @@
#[perlmod::package(name = "Proxmox::RS::Notify")]
mod export {
use std::collections::HashMap;
use std::sync::Mutex;
use anyhow::{bail, Error};
use serde_json::Value as JSONValue;
use perlmod::Value;
use proxmox_http_error::HttpError;
use proxmox_notify::endpoints::gotify::{
DeleteableGotifyProperty, GotifyConfig, GotifyConfigUpdater, GotifyPrivateConfig,
GotifyPrivateConfigUpdater,
};
use proxmox_notify::endpoints::sendmail::{
DeleteableSendmailProperty, SendmailConfig, SendmailConfigUpdater,
};
use proxmox_notify::endpoints::smtp::{
DeleteableSmtpProperty, SmtpConfig, SmtpConfigUpdater, SmtpMode, SmtpPrivateConfig,
SmtpPrivateConfigUpdater,
};
use proxmox_notify::matcher::{
CalendarMatcher, DeleteableMatcherProperty, FieldMatcher, MatchModeOperator, MatcherConfig,
MatcherConfigUpdater, SeverityMatcher,
};
use proxmox_notify::{api, Config, Notification, Severity};
pub struct NotificationConfig {
config: Mutex<Config>,
}
perlmod::declare_magic!(Box<NotificationConfig> : &NotificationConfig as "Proxmox::RS::Notify");
/// Support `dclone` so this can be put into the `ccache` of `PVE::Cluster`.
#[export(name = "STORABLE_freeze", raw_return)]
fn storable_freeze(
#[try_from_ref] this: &NotificationConfig,
cloning: bool,
) -> Result<Value, Error> {
if !cloning {
bail!("freezing Notification config not supported!");
}
let mut cloned = Box::new(NotificationConfig {
config: Mutex::new(this.config.lock().unwrap().clone()),
});
let value = Value::new_pointer::<NotificationConfig>(&mut *cloned);
let _perl = Box::leak(cloned);
Ok(value)
}
/// Instead of `thaw` we implement `attach` for `dclone`.
#[export(name = "STORABLE_attach", raw_return)]
fn storable_attach(
#[raw] class: Value,
cloning: bool,
#[raw] serialized: Value,
) -> Result<Value, Error> {
if !cloning {
bail!("STORABLE_attach called with cloning=false");
}
let data = unsafe { Box::from_raw(serialized.pv_raw::<NotificationConfig>()?) };
Ok(perlmod::instantiate_magic!(&class, MAGIC => data))
}
#[export(raw_return)]
fn parse_config(
#[raw] class: Value,
raw_config: &[u8],
raw_private_config: &[u8],
) -> Result<Value, Error> {
let raw_config = std::str::from_utf8(raw_config)?;
let raw_private_config = std::str::from_utf8(raw_private_config)?;
Ok(perlmod::instantiate_magic!(&class, MAGIC => Box::new(
NotificationConfig {
config: Mutex::new(Config::new(raw_config, raw_private_config)?)
}
)))
}
#[export]
fn write_config(#[try_from_ref] this: &NotificationConfig) -> Result<(String, String), Error> {
Ok(this.config.lock().unwrap().write()?)
}
#[export]
fn digest(#[try_from_ref] this: &NotificationConfig) -> String {
let config = this.config.lock().unwrap();
hex::encode(config.digest())
}
#[export(serialize_error)]
fn send(
#[try_from_ref] this: &NotificationConfig,
severity: Severity,
title: String,
body: String,
template_data: Option<JSONValue>,
fields: Option<HashMap<String, String>>,
) -> Result<(), HttpError> {
let config = this.config.lock().unwrap();
let notification = Notification::new_templated(
severity,
title,
body,
template_data.unwrap_or_default(),
fields.unwrap_or_default(),
);
api::common::send(&config, &notification)
}
#[export(serialize_error)]
fn test_target(
#[try_from_ref] this: &NotificationConfig,
target: &str,
) -> Result<(), HttpError> {
let config = this.config.lock().unwrap();
api::common::test_target(&config, target)
}
#[export(serialize_error)]
fn get_sendmail_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<SendmailConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::sendmail::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<SendmailConfig, HttpError> {
let config = this.config.lock().unwrap();
api::sendmail::get_endpoint(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::sendmail::add_endpoint(
&mut config,
&SendmailConfig {
name,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
filter: None,
origin: None,
},
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableSendmailProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::sendmail::update_endpoint(
&mut config,
name,
&SendmailConfigUpdater {
mailto,
mailto_user,
from_address,
author,
comment,
disable,
},
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_sendmail_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::sendmail::delete_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_gotify_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<GotifyConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::gotify::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<GotifyConfig, HttpError> {
let config = this.config.lock().unwrap();
api::gotify::get_endpoint(&config, id)
}
#[export(serialize_error)]
fn add_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
server: String,
token: String,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::gotify::add_endpoint(
&mut config,
&GotifyConfig {
name: name.clone(),
server,
comment,
disable,
filter: None,
origin: None,
},
&GotifyPrivateConfig { name, token },
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
server: Option<String>,
token: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableGotifyProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::gotify::update_endpoint(
&mut config,
name,
&GotifyConfigUpdater {
server,
comment,
disable,
},
&GotifyPrivateConfigUpdater { token },
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_gotify_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::gotify::delete_gotify_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_smtp_endpoints(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<SmtpConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::smtp::get_endpoints(&config)
}
#[export(serialize_error)]
fn get_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<SmtpConfig, HttpError> {
let config = this.config.lock().unwrap();
api::smtp::get_endpoint(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: String,
server: String,
port: Option<u16>,
mode: Option<SmtpMode>,
username: Option<String>,
password: Option<String>,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: String,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::smtp::add_endpoint(
&mut config,
&SmtpConfig {
name: name.clone(),
server,
port,
mode,
username,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
origin: None,
},
&SmtpPrivateConfig { name, password },
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
server: Option<String>,
port: Option<u16>,
mode: Option<SmtpMode>,
username: Option<String>,
password: Option<String>,
mailto: Option<Vec<String>>,
mailto_user: Option<Vec<String>>,
from_address: Option<String>,
author: Option<String>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableSmtpProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::smtp::update_endpoint(
&mut config,
name,
&SmtpConfigUpdater {
server,
port,
mode,
username,
mailto,
mailto_user,
from_address,
author,
comment,
disable,
},
&SmtpPrivateConfigUpdater { password },
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_smtp_endpoint(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::smtp::delete_endpoint(&mut config, name)
}
#[export(serialize_error)]
fn get_matchers(
#[try_from_ref] this: &NotificationConfig,
) -> Result<Vec<MatcherConfig>, HttpError> {
let config = this.config.lock().unwrap();
api::matcher::get_matchers(&config)
}
#[export(serialize_error)]
fn get_matcher(
#[try_from_ref] this: &NotificationConfig,
id: &str,
) -> Result<MatcherConfig, HttpError> {
let config = this.config.lock().unwrap();
api::matcher::get_matcher(&config, id)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn add_matcher(
#[try_from_ref] this: &NotificationConfig,
name: String,
target: Option<Vec<String>>,
match_severity: Option<Vec<SeverityMatcher>>,
match_field: Option<Vec<FieldMatcher>>,
match_calendar: Option<Vec<CalendarMatcher>>,
mode: Option<MatchModeOperator>,
invert_match: Option<bool>,
comment: Option<String>,
disable: Option<bool>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::matcher::add_matcher(
&mut config,
&MatcherConfig {
name,
match_severity,
match_field,
match_calendar,
target,
mode,
invert_match,
comment,
disable,
origin: None,
},
)
}
#[export(serialize_error)]
#[allow(clippy::too_many_arguments)]
fn update_matcher(
#[try_from_ref] this: &NotificationConfig,
name: &str,
target: Option<Vec<String>>,
match_severity: Option<Vec<SeverityMatcher>>,
match_field: Option<Vec<FieldMatcher>>,
match_calendar: Option<Vec<CalendarMatcher>>,
mode: Option<MatchModeOperator>,
invert_match: Option<bool>,
comment: Option<String>,
disable: Option<bool>,
delete: Option<Vec<DeleteableMatcherProperty>>,
digest: Option<&str>,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
let digest = decode_digest(digest)?;
api::matcher::update_matcher(
&mut config,
name,
&MatcherConfigUpdater {
match_severity,
match_field,
match_calendar,
target,
mode,
invert_match,
comment,
disable,
},
delete.as_deref(),
digest.as_deref(),
)
}
#[export(serialize_error)]
fn delete_matcher(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<(), HttpError> {
let mut config = this.config.lock().unwrap();
api::matcher::delete_matcher(&mut config, name)
}
#[export]
fn get_referenced_entities(
#[try_from_ref] this: &NotificationConfig,
name: &str,
) -> Result<Vec<String>, HttpError> {
let config = this.config.lock().unwrap();
api::common::get_referenced_entities(&config, name)
}
fn decode_digest(digest: Option<&str>) -> Result<Option<Vec<u8>>, HttpError> {
digest
.map(hex::decode)
.transpose()
.map_err(|e| api::http_err!(BAD_REQUEST, "invalid digest: {e}"))
}
}

5
pmg-rs/.cargo/config Normal file
View File

@ -0,0 +1,5 @@
[source]
[source.debian-packages]
directory = "/usr/share/cargo/registry"
[source.crates-io]
replace-with = "debian-packages"

View File

@ -1,14 +1,13 @@
[package]
name = "pmg-rs"
version = "0.6.2"
authors = [
"Proxmox Support Team <support@proxmox.com>",
"Wolfgang Bumiller <w.bumiller@proxmox.com>",
"Fabian Ebner <f.ebner@proxmox.com>",
]
edition = "2018"
license = "AGPL-3"
version = "0.7.5"
description = "PMG parts which have been ported to rust"
homepage = "https://www.proxmox.com"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2021"
license = "AGPL-3"
repository = "https://git.proxmox.com/?p=proxmox.git"
exclude = [
"build",
"debian",
@ -20,22 +19,25 @@ crate-type = [ "cdylib" ]
[dependencies]
anyhow = "1.0"
env_logger = "0.10"
hex = "0.4"
http = "0.2.7"
libc = "0.2"
nix = "0.24"
nix = "0.26"
openssl = "0.10.40"
serde = "1.0"
serde_bytes = "0.11.3"
serde_bytes = "0.11"
serde_json = "1.0"
url = "2"
perlmod = { version = "0.13", features = [ "exporter" ] }
perlmod = { version = "0.13.4", features = [ "exporter" ] }
proxmox-acme-rs = { version = "0.4", features = ["client"] }
proxmox-apt = "0.9"
proxmox-http = { version = "0.7", features = ["client-sync", "client-trait"] }
proxmox-subscription = "0.3"
proxmox-sys = "0.4"
proxmox-tfa = { version = "2.1", features = ["api"] }
proxmox-acme = { version = "0.5", features = ["client"] }
proxmox-apt = "0.10"
proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-http-error = "0.1.0"
proxmox-notify = "0.3.1"
proxmox-subscription = "0.4"
proxmox-sys = "0.5"
proxmox-tfa = { version = "4.0.4", features = ["api"] }
proxmox-time = "1.1.3"

4
pmg-rs/Fixup.pm Normal file
View File

@ -0,0 +1,4 @@
# BEGIN Fixup.pm
# This is prepended to the current PMG.pm to force-include the temporary `openssl-probe` fixup.
use Proxmox::Lib::SslProbe;
# END Fixup.pm

View File

@ -1,4 +1,4 @@
#include /usr/share/dpkg/default.mk
#include /usr/share/dpkg/pkg-info.mk
PACKAGE=libpmg-rs-perl
@ -8,33 +8,46 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb
DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb
MAIN_DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DBGSYM_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(ARCH).deb
DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR=
PM_DIR := PMG
PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=pmg_rs \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::PMG \
--lib-prefix=PMG \
--include-file=Fixup.pm
PERLMOD_PACKAGES := \
PMG::RS::APT::Repositories \
PMG::RS::Acme \
PMG::RS::CSR \
PMG::RS::OpenId \
PMG::RS::TFA
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline
TARGET_DIR=release
else
TARGET_DIR=debug
endif
all:
ifneq ($(BUILD_MODE), skip)
all: PMG
cargo build $(CARGO_BUILD_ARGS)
endif
# always re-create this dir
# but also copy the local target/ and PMG/ dirs as a build-cache
.PHONY: build
build:
rm -rf build
cargo build --release --offline
rsync -a debian Makefile Cargo.toml Cargo.lock src target PMG build/
Proxmox PMG: Proxmox/Lib/PMG.pm
Proxmox/Lib/PMG.pm: Fixup.pm
$(PERLMOD_GENPACKAGE) $(PERLMOD_PACKAGES)
.PHONY: install
install: target/release/libpmg_rs.so
install: target/release/libpmg_rs.so Proxmox/Lib/PMG.pm PMG
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
install -m644 target/release/libpmg_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpmg_rs.so
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
@ -42,25 +55,43 @@ install: target/release/libpmg_rs.so
install -m644 Proxmox/Lib/PMG.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PMG.pm
find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
.PHONY: deb
deb: $(MAIN_DEB)
$(MAIN_DEB): build
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS)
distclean: clean
clean:
rm -rf PMG Proxmox
cargo clean
rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build
find . -name '*~' -exec rm {} ';'
rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes Cargo.lock
rm -rf $(PACKAGE)-[0-9]*/
.PHONY: dinstall
dinstall: ${DEBS}
dpkg -i ${DEBS}
dinstall: $(DEBS)
dpkg -i $(DEBS)
.PHONY: upload
upload: ${DEBS}
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEBS)
# check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pmg --dist bullseye
tar cf - $(DEBS) | ssh -X repoman@repo.proxmox.com upload --product pmg --dist $(DEB_DISTRIBUTION)
$(BUILDDIR): src debian common/src Cargo.toml Makefile .cargo/config
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp/common
cp -a -t $(BUILDDIR).tmp src debian Cargo.toml Makefile .cargo Fixup.pm
cp -a -t $(BUILDDIR).tmp/common common/src
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb
deb: $(DEBS)
$(DEBS): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -b -us -uc
lintian $(DEBS)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)

1
pmg-rs/common Symbolic link
View File

@ -0,0 +1 @@
../common

View File

@ -1,3 +1,49 @@
libpmg-rs-perl (0.7.5) bookworm; urgency=medium
* add EAB binding support to ACME
* make Proxmox::Lib::PMG pull in Proxmox::Lib::SslProbe to work around
an issue where the openssl-probe crate calls setenv() and messes up perl's
view of the environment
-- Proxmox Support Team <support@proxmox.com> Thu, 07 Dec 2023 09:57:43 +0100
libpmg-rs-perl (0.7.4) bookworm; urgency=medium
* update to env logger 0.10
* use declare_magic for ACME
* add Promox::Lib::PMG::use_safe_putenv
-- Proxmox Support Team <support@proxmox.com> Wed, 06 Dec 2023 11:22:56 +0100
libpmg-rs-perl (0.7.3) bookworm; urgency=medium
* reset failure counts when unlocking second factors
-- Proxmox Support Team <support@proxmox.com> Wed, 05 Jul 2023 13:35:23 +0200
libpmg-rs-perl (0.7.2) bookworm; urgency=medium
* set default log level to 'info'
* introduce PMG_LOG environment variable to override log level
* add tfa_lock_status query sub
* add api_unlock_tfa sub
* bump proxmox-tfa to 4.0.2
-- Proxmox Support Team <support@proxmox.com> Tue, 27 Jun 2023 16:01:23 +0200
libpmg-rs-perl (0.7.1) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Thu, 18 May 2023 12:01:08 +0200
libpmg-rs-perl (0.6.2) bullseye; urgency=medium
* update to proxmox-subscription 0.3 / proxmox-http 0.7

View File

@ -1 +0,0 @@
12

View File

@ -1,43 +1,51 @@
Source: libpmg-rs-perl
Section: perl
Priority: optional
Build-Depends: cargo:native <!nocheck>,
debhelper-compat (= 13),
librust-openssl-probe-dev (= 0.1.5-1~bpo12+pve1),
dh-cargo (>= 25),
librust-anyhow-1+default-dev,
librust-env-logger-0.10+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.26+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev (>= 0.13.4-~~),
librust-perlmod-0.13+exporter-dev (>= 0.13.4-~~),
librust-proxmox-acme-0.5+client-dev,
librust-proxmox-acme-0.5+default-dev,
librust-proxmox-apt-0.10+default-dev,
librust-proxmox-http-0.9+client-sync-dev,
librust-proxmox-http-0.9+client-trait-dev,
librust-proxmox-http-0.9+default-dev,
librust-proxmox-http-error-0.1+default-dev,
librust-proxmox-notify-0.3+default-dev (>= 0.3.1-~~),
librust-proxmox-subscription-0.4+default-dev,
librust-proxmox-sys-0.5+default-dev,
librust-proxmox-tfa-4+api-dev (>= 4.0.4-~~),
librust-proxmox-tfa-4+default-dev (>= 4.0.4-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
libstd-rust-dev <!nocheck>,
perlmod-bin (>= 0.2.0-3),
rustc:native <!nocheck>,
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends:
debhelper (>= 12),
dh-cargo (>= 24),
cargo:native <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.24+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev,
librust-perlmod-0.13+exporter-dev,
librust-proxmox-acme-rs-0.4+client-dev,
librust-proxmox-acme-rs-0.4+default-dev,
librust-proxmox-apt-0.9+default-dev,
librust-proxmox-http-0.7+client-sync-dev,
librust-proxmox-http-0.7+client-trait-dev,
librust-proxmox-http-0.7+default-dev,
librust-proxmox-subscription-0.3+default-dev,
librust-proxmox-sys-0.4+default-dev,
librust-proxmox-tfa-2+api-dev (>= 2.1-~~),
librust-proxmox-tfa-2+default-dev (>= 2.1-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev (>= 0.11.3-~~),
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
Standards-Version: 4.3.0
Standards-Version: 4.6.1
Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
Homepage: https://www.proxmox.com
Package: libpmg-rs-perl
Architecture: any
Depends: ${perl:Depends},
Depends: ${misc:Depends},
${perl:Depends},
${shlibs:Depends},
libproxmox-rs-perl (>= 0.3.3),
Description: Components of Proxmox Mail Gateway which have been ported to Rust.
Contains parts of Proxmox Mail Gateway which have been ported to, or newly
implemented in the Rust programming language.

View File

@ -1,10 +1,31 @@
# WARNING: this is *NOT* use as canonical source for d/control, but rather occasionally used via
# an invocation like:
# make clean
# rm debian/control
# debcargo package --config debian/debcargo.toml --changelog-ready --no-overlay-write-back --directory libpmg-rs-perl-0.7.1 pmg-rs 0.7.1
# mv libpmg-rs-perl-0.7.1/debian/control debian/control
# to semi.manually refresh the control file
#
# NOTE: debcargo thinks this is a source package, but it isn't! Drop provides, the dependencies of
# the binary package on rust source packages, Multi-Arch same, and other things that do not make
# sense for a combined perl + arch-dependent library package.
overlay = "."
crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>"
[source]
section = "perl"
vcs_git = "git://git.proxmox.com/git/proxmox.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
vcs_git = "git://git.proxmox.com/git/proxmox-perl-rs.git"
vcs_browser = "https://git.proxmox.com/?p=proxmox-perl-rs.git"
build_depends = [
"perlmod-bin",
]
[packages.libpmg-rs-perl]
[packages.bin]
name = "libpmg-rs-perl"
summary = "Components of Proxmox Mail Gateway which have been ported to Rust."
description = """
Contains parts of Proxmox Mail Gateway which have been ported to, or newly
implemented in the Rust programming language.
"""

View File

@ -1,7 +1,25 @@
#!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
include /usr/share/rustc/architecture.mk
#export DH_VERBOSE=1
export BUILD_MODE=release
CARGO=/usr/share/cargo/bin/cargo
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
export CARGO_HOME = $(CURDIR)/debian/cargo_home
export DEB_CARGO_CRATE=pmg-rs_$(DEB_VERSION_UPSTREAM)
export DEB_CARGO_PACKAGE=pmg-rs
%:
dh $@
override_dh_auto_configure:
@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
dh_auto_configure

View File

@ -9,8 +9,8 @@ use std::os::unix::fs::OpenOptionsExt;
use anyhow::{format_err, Error};
use serde::{Deserialize, Serialize};
use proxmox_acme_rs::account::AccountData as AcmeAccountData;
use proxmox_acme_rs::{Account, Client};
use proxmox_acme::account::AccountData as AcmeAccountData;
use proxmox_acme::{Account, Client};
/// Our on-disk format inherited from PVE's proxmox-acme code.
#[derive(Deserialize, Serialize)]
@ -79,6 +79,7 @@ impl Inner {
tos_agreed: bool,
contact: Vec<String>,
rsa_bits: Option<u32>,
eab_creds: Option<(String, String)>,
) -> Result<(), Error> {
self.tos = if tos_agreed {
self.client.terms_of_service_url()?.map(str::to_owned)
@ -86,7 +87,9 @@ impl Inner {
None
};
let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?;
let _account = self
.client
.new_account(contact, tos_agreed, rsa_bits, eab_creds)?;
let file = OpenOptions::new()
.write(true)
.create(true)
@ -182,67 +185,45 @@ impl Inner {
#[perlmod::package(name = "PMG::RS::Acme")]
pub mod export {
use std::collections::HashMap;
use std::convert::TryFrom;
use std::sync::Mutex;
use anyhow::Error;
use serde_bytes::{ByteBuf, Bytes};
use perlmod::Value;
use proxmox_acme_rs::directory::Meta;
use proxmox_acme_rs::order::OrderData;
use proxmox_acme_rs::{Authorization, Challenge, Order};
use proxmox_acme::directory::Meta;
use proxmox_acme::order::OrderData;
use proxmox_acme::{Authorization, Challenge, Order};
use super::{AccountData, Inner};
const CLASSNAME: &str = "PMG::RS::Acme";
perlmod::declare_magic!(Box<Acme> : &Acme as "PMG::RS::Acme");
/// An Acme client instance.
pub struct Acme {
inner: Mutex<Inner>,
}
impl<'a> TryFrom<&'a Value> for &'a Acme {
type Error = Error;
fn try_from(value: &'a Value) -> Result<&'a Acme, Error> {
Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
}
}
fn bless(class: Value, mut ptr: Box<Acme>) -> Result<Value, Error> {
let value = Value::new_pointer::<Acme>(&mut *ptr);
let value = Value::new_ref(&value);
let this = value.bless_sv(&class)?;
let _perl = Box::leak(ptr);
Ok(this)
}
/// Create a new ACME client instance given an account path and an API directory URL.
#[export(raw_return)]
pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> {
bless(
class,
Box::new(Acme {
Ok(perlmod::instantiate_magic!(
&class,
MAGIC => Box::new(Acme {
inner: Mutex::new(Inner::new(api_directory)?),
}),
)
})
))
}
/// Load an existing account.
#[export(raw_return)]
pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> {
bless(
class,
Box::new(Acme {
Ok(perlmod::instantiate_magic!(
&class,
MAGIC => Box::new(Acme {
inner: Mutex::new(Inner::load(account_path)?),
}),
)
}
#[export(name = "DESTROY")]
fn destroy(#[raw] this: Value) {
perlmod::destructor!(this, Acme: CLASSNAME);
})
))
}
/// Create a new account.
@ -260,11 +241,16 @@ pub mod export {
tos_agreed: bool,
contact: Vec<String>,
rsa_bits: Option<u32>,
eab_kid: Option<String>,
eab_hmac_key: Option<String>,
) -> Result<(), Error> {
this.inner
.lock()
.unwrap()
.new_account(account_path, tos_agreed, contact, rsa_bits)
this.inner.lock().unwrap().new_account(
account_path,
tos_agreed,
contact,
rsa_bits,
eab_kid.zip(eab_hmac_key),
)
}
/// Get the directory's meta information.

View File

@ -5,7 +5,7 @@ pub mod export {
use anyhow::Error;
use serde_bytes::ByteBuf;
use proxmox_acme_rs::util::Csr;
use proxmox_acme::util::Csr;
/// Generates a CSR and its accompanying private key.
///

View File

@ -1,7 +1,25 @@
#[path = "../../common/src/mod.rs"]
#[path = "../common/src/mod.rs"]
pub mod common;
pub mod acme;
pub mod apt;
pub mod csr;
pub mod tfa;
#[perlmod::package(name = "Proxmox::Lib::PMG", lib = "pmg_rs")]
mod export {
use crate::common;
#[export]
pub fn init() {
common::logger::init("PMG_LOG", "info");
}
/// CLI tools should call this very early. This is a workaround causing environment variable
/// manipulation to leak instead of crash. Required when calling into rust code that causes
/// `setenv` calls, particularly code using the openssl crate.
#[export]
pub fn use_safe_putenv() {
perlmod::ffi::use_safe_putenv(true);
}
}

View File

@ -18,11 +18,13 @@ use nix::errno::Errno;
use nix::sys::stat::Mode;
pub(self) use proxmox_tfa::api::{
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, WebauthnConfig,
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, UserChallengeAccess,
WebauthnConfig,
};
#[perlmod::package(name = "PMG::RS::TFA")]
mod export {
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::Mutex;
@ -31,7 +33,7 @@ mod export {
use url::Url;
use perlmod::Value;
use proxmox_tfa::api::methods;
use proxmox_tfa::api::{methods, TfaResult};
use super::{TfaConfig, UserAccess};
@ -105,7 +107,7 @@ mod export {
) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_challenge(UserAccess::new(&raw_this)?, userid, description)
inner.u2f_registration_challenge(&UserAccess::new(&raw_this)?, userid, description)
}
/// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
@ -120,7 +122,7 @@ mod export {
) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_finish(UserAccess::new(&raw_this)?, userid, challenge, response)
inner.u2f_registration_finish(&UserAccess::new(&raw_this)?, userid, challenge, response)
}
/// Check if a user has any TFA entries of a given type.
@ -203,7 +205,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
match inner.authentication_challenge(
UserAccess::new(&raw_this)?,
&UserAccess::new(&raw_this)?,
userid,
origin.as_ref(),
)? {
@ -220,10 +222,7 @@ mod export {
.unwrap()
.users
.get(userid)
.and_then(|user| {
let state = user.recovery_state();
state.is_available().then(move || state)
})
.and_then(|user| user.recovery_state())
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
@ -244,15 +243,78 @@ mod export {
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
inner
.verify(
UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
)
.map(|save| save.needs_saving())
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
match result {
TfaResult::Success { needs_saving } => Ok(needs_saving),
_ => bail!("TFA authentication failed"),
}
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
/// it.
///
/// Returns a result hash of the form:
/// ```text
/// {
/// "result": bool, // whether TFA was successful
/// "needs-saving": bool, // whether the user config needs saving
/// "tfa-limit-reached": bool, // whether the TFA limit was reached (config needs saving)
/// "totp-limit-reached": bool, // whether the TOTP limit was reached (config needs saving)
/// }
/// ```
#[export]
fn authentication_verify2(
#[raw] raw_this: Value,
//#[try_from_ref] this: &Tfa,
userid: &str,
challenge: &str, //super::TfaChallenge,
response: &str,
origin: Option<Url>,
) -> Result<TfaReturnValue, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
Ok(match result {
TfaResult::Success { needs_saving } => TfaReturnValue {
result: true,
needs_saving,
..Default::default()
},
TfaResult::Locked => TfaReturnValue::default(),
TfaResult::Failure {
needs_saving,
totp_limit_reached,
tfa_limit_reached,
} => TfaReturnValue {
result: false,
needs_saving,
totp_limit_reached,
tfa_limit_reached,
},
})
}
#[derive(Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaReturnValue {
result: bool,
needs_saving: bool,
totp_limit_reached: bool,
tfa_limit_reached: bool,
}
/// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
@ -314,7 +376,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?;
methods::add_tfa_entry(
&mut this.inner.lock().unwrap(),
UserAccess::new(&raw_this)?,
&UserAccess::new(&raw_this)?,
userid,
description,
totp,
@ -375,6 +437,66 @@ mod export {
Err(methods::EntryNotFound) => bail!("no such entry"),
}
}
#[export]
fn api_unlock_tfa(#[raw] raw_this: Value, userid: &str) -> Result<bool, Error> {
let this: &Tfa = (&raw_this).try_into()?;
Ok(methods::unlock_and_reset_tfa(
&mut this.inner.lock().unwrap(),
&UserAccess::new(&raw_this)?,
userid,
)?)
}
#[derive(serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaLockStatus {
/// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use
/// a recovery key.
#[serde(skip_serializing_if = "bool_is_false", default)]
totp_locked: bool,
/// If a user hits too many 2nd factor failures, they get completely blocked for a while.
#[serde(skip_serializing_if = "Option::is_none", default)]
#[serde(deserialize_with = "filter_expired_timestamp")]
tfa_locked_until: Option<i64>,
}
impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus {
fn from(data: &proxmox_tfa::api::TfaUserData) -> Self {
Self {
totp_locked: data.totp_locked,
tfa_locked_until: data.tfa_locked_until,
}
}
}
fn bool_is_false(b: &bool) -> bool {
!*b
}
#[export]
fn tfa_lock_status(
#[try_from_ref] this: &Tfa,
userid: Option<&str>,
) -> Result<Option<perlmod::Value>, Error> {
let this = this.inner.lock().unwrap();
if let Some(userid) = userid {
if let Some(user) = this.users.get(userid) {
Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?))
} else {
Ok(None)
}
} else {
Ok(Some(perlmod::to_value(
&HashMap::<String, TfaLockStatus>::from_iter(
this.users
.iter()
.map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))),
),
)?))
}
}
}
/// Attach the path to errors from [`nix::mkir()`].
@ -440,9 +562,7 @@ fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
}
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
type Data = UserChallengeData;
fn open(&self, userid: &str) -> Result<UserChallengeData, Error> {
fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
if self.is_debug() {
mkdir("./local-tfa-challenges", 0o700)?;
} else {
@ -485,15 +605,15 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
}
};
Ok(UserChallengeData {
Ok(Box::new(UserChallengeData {
inner,
path,
lock: file,
})
}))
}
/// `open` without creating the file if it doesn't exist, to finish WA authentications.
fn open_no_create(&self, userid: &str) -> Result<Option<UserChallengeData>, Error> {
fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
let path = challenge_data_path(userid, self.is_debug());
let mut file = match std::fs::OpenOptions::new()
@ -514,11 +634,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
format_err!("failed to read challenge data for user {}: {}", userid, err)
})?;
Ok(Some(UserChallengeData {
Ok(Some(Box::new(UserChallengeData {
inner,
path,
lock: file,
}))
})))
}
fn remove(&self, userid: &str) -> Result<bool, Error> {
@ -529,6 +649,10 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
Err(err) => Err(err.into()),
}
}
fn enable_lockout(&self) -> bool {
true
}
}
/// Container of `TfaUserChallenges` with the corresponding file lock guard.
@ -546,7 +670,7 @@ impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
&mut self.inner
}
fn save(self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
UserChallengeData::save(self)
}
}
@ -591,7 +715,7 @@ impl UserChallengeData {
///
/// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
/// way also unlocks early.
fn save(mut self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
self.rewind()?;
serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {

View File

@ -1,11 +1,12 @@
[package]
name = "pve-rs"
version = "0.7.3"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2018"
license = "AGPL-3"
version = "0.8.8"
description = "PVE parts which have been ported to Rust"
homepage = "https://www.proxmox.com"
authors = ["Proxmox Support Team <support@proxmox.com>"]
edition = "2021"
license = "AGPL-3"
repository = "https://git.proxmox.com/?p=proxmox.git"
exclude = [
"debian",
@ -18,10 +19,12 @@ crate-type = [ "cdylib" ]
anyhow = "1.0"
base32 = "0.4"
base64 = "0.13"
env_logger = "0.10"
hex = "0.4"
http = "0.2.7"
libc = "0.2"
nix = "0.24"
log = "0.4.17"
nix = "0.26"
openssl = "0.10.40"
serde = "1.0"
serde_bytes = "0.11"
@ -30,11 +33,13 @@ url = "2"
perlmod = { version = "0.13", features = [ "exporter" ] }
proxmox-apt = "0.9"
proxmox-http = { version = "0.7", features = ["client-sync", "client-trait"] }
proxmox-openid = "0.9.5"
proxmox-resource-scheduling = "0.1"
proxmox-subscription = "0.3"
proxmox-sys = "0.4"
proxmox-tfa = { version = "2.1", features = ["api"] }
proxmox-apt = "0.10.6"
proxmox-http = { version = "0.9", features = ["client-sync", "client-trait"] }
proxmox-http-error = "0.1.0"
proxmox-notify = { version = "0.3.1", features = ["pve-context"] }
proxmox-openid = "0.10"
proxmox-resource-scheduling = "0.3.0"
proxmox-subscription = "0.4"
proxmox-sys = "0.5"
proxmox-tfa = { version = "4.0.4", features = ["api"] }
proxmox-time = "1.1.3"

4
pve-rs/Fixup.pm Normal file
View File

@ -0,0 +1,4 @@
# BEGIN Fixup.pm
# This is prepended to the current PMG.pm to force-include the temporary `openssl-probe` fixup.
use Proxmox::Lib::SslProbe;
# END Fixup.pm

View File

@ -1,4 +1,4 @@
#include /usr/share/dpkg/default.mk
#include /usr/share/dpkg/pkg-info.mk
PACKAGE=libpve-rs-perl
export PERLMOD_PRODUCT=PVE
@ -9,33 +9,52 @@ export GITVERSION:=$(shell git rev-parse HEAD)
PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb
DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb
MAIN_DEB=$(PACKAGE)_$(DEB_VERSION)_$(ARCH).deb
DBGSYM_DEB=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(ARCH).deb
DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
DSC=$(PACKAGE)_$(DEB_VERSION_UPSTREAM_REVISION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
DESTDIR=
PM_DIR := PVE
PERLMOD_GENPACKAGE := /usr/lib/perlmod/genpackage.pl \
--lib=pve_rs \
--lib-tag=proxmox \
--lib-package=Proxmox::Lib::PVE \
--lib-prefix=PVE \
--include-file=Fixup.pm
PERLMOD_PACKAGES := \
PVE::RS::APT::Repositories \
PVE::RS::OpenId \
PVE::RS::ResourceScheduling::Static \
PVE::RS::TFA
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release --offline
TARGET_DIR=release
else
TARGET_DIR=debug
endif
all:
ifneq ($(BUILD_MODE), skip)
all: PVE
cargo build $(CARGO_BUILD_ARGS)
endif
mkdir -p test/Proxmox/Lib
sed -r -e \
's@^sub libfile.*$$@sub libfile { "$(shell pwd)/target/$(TARGET_DIR)/libpve_rs.so"; }@' \
Proxmox/Lib/PVE.pm >test/Proxmox/Lib/PVE.pm
# always re-create this dir
# but also copy the local target/ and PVE/ dirs as a build-cache
.PHONY: build
build:
rm -rf build
cargo build --release --offline
rsync -a debian Makefile Cargo.toml Cargo.lock src target PVE build/
Proxmox PVE: Proxmox/Lib/PVE.pm
Proxmox/Lib/PVE.pm: Fixup.pm
$(PERLMOD_GENPACKAGE) $(PERLMOD_PACKAGES)
check: all
$(MAKE) -C test test
.PHONY: install
install: target/release/libpve_rs.so
install: target/release/libpve_rs.so Proxmox/Lib/PVE.pm PVE
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
install -m644 target/release/libpve_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpve_rs.so
install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)
@ -43,25 +62,42 @@ install: target/release/libpve_rs.so
install -m644 Proxmox/Lib/PVE.pm $(DESTDIR)$(PERL_INSTALLVENDORLIB)/Proxmox/Lib/PVE.pm
find $(PM_DIR) \! -type d -print -exec install -Dm644 '{}' $(DESTDIR)$(PERL_INSTALLVENDORLIB)'/{}' ';'
.PHONY: deb
deb: $(MAIN_DEB)
$(MAIN_DEB): build
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
lintian $(DEBS)
distclean: clean
clean:
rm -rf PVE Proxmox
cargo clean
rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build
find . -name '*~' -exec rm {} ';'
rm -f *.deb *.dsc *.tar.* *.build *.buildinfo *.changes Cargo.lock
rm -rf $(PACKAGE)-[0-9]*/
.PHONY: dinstall
dinstall: ${DEBS}
dpkg -i ${DEBS}
dinstall: $(DEBS)
dpkg -i $(DEBS)
.PHONY: upload
upload: ${DEBS}
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEBS)
# check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pve --dist bullseye
tar cf - $(DEBS) | ssh -X repoman@repo.proxmox.com upload --product pve --dist $(DEB_DISTRIBUTION)
$(BUILDDIR): src debian test common/src Cargo.toml Makefile .cargo/config
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp
mkdir $(BUILDDIR).tmp/common
cp -a -t $(BUILDDIR).tmp src debian test Cargo.toml Makefile .cargo Fixup.pm
cp -a -t $(BUILDDIR).tmp/common common/src
mv $(BUILDDIR).tmp $(BUILDDIR)
.PHONY: deb
deb: $(DEBS)
$(DEBS): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -b -us -uc
lintian $(DEBS)
.PHONY: dsc
dsc: $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); PATH="/usr/local/bin:/usr/bin" dpkg-buildpackage -S -us -uc -d
lintian $(DSC)
sbuild: $(DSC)
sbuild $(DSC)

1
pve-rs/common Symbolic link
View File

@ -0,0 +1 @@
../common

View File

@ -1,3 +1,114 @@
libpve-rs-perl (0.8.8) bookworm; urgency=medium
* notify: include 'hostname' and 'type' metadata fields for forwarded mails
* notify: smtp: forward original message instead of nesting
* notify: smtp: add 'Auto-Submitted' header to email body
* notify: api: allow resetting built-in targets if used by a matcher
-- Proxmox Support Team <support@proxmox.com> Wed, 10 Jan 2024 14:19:47 +0100
libpve-rs-perl (0.8.7) bookworm; urgency=medium
* notify: adapt to new matcher-based notification routing
* notify: add bindings for smtp API calls
* pve-rs: notify: remove notify_context for PVE
* notify: add 'disable' parameter
* notify: support 'origin' paramter
-- Proxmox Support Team <support@proxmox.com> Fri, 17 Nov 2023 13:41:17 +0100
libpve-rs-perl (0.8.6) bookworm; urgency=medium
* re-build with newer proxmox-apt depenceny to make Ceph Reef repo available
-- Proxmox Support Team <support@proxmox.com> Tue, 05 Sep 2023 15:37:44 +0200
libpve-rs-perl (0.8.5) bookworm; urgency=medium
* add PVE::RS::Notify module
-- Proxmox Support Team <support@proxmox.com> Mon, 24 Jul 2023 11:18:56 +0200
libpve-rs-perl (0.8.4) bookworm; urgency=medium
* reset failure counts when unlocking second factors
-- Proxmox Support Team <support@proxmox.com> Wed, 05 Jul 2023 13:30:17 +0200
libpve-rs-perl (0.8.3) bookworm; urgency=medium
* set default log level to 'info'
* introduce PVE_LOG environment variable to override log level
* add tfa_lock_status query sub
* bump proxmox-tfa to 4.0.2
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jun 2023 12:55:03 +0200
libpve-rs-perl (0.8.2) bookworm; urgency=medium
* update proxmox-apt which updated repositories info for bookworm
-- Proxmox Support Team <support@proxmox.com> Sun, 04 Jun 2023 18:33:42 +0200
libpve-rs-perl (0.8.1) bookworm; urgency=medium
* bump proxmox-apt,http,openid,subscription,sys crates to their bookworm
versions
* bump proxmox-tfa to 4.0.1 to include the unlock API
* enable TFA lockout and provide the `api_unlock_tfa` call
-- Proxmox Support Team <support@proxmox.com> Wed, 31 May 2023 14:17:31 +0200
libpve-rs-perl (0.8.0) bookworm; urgency=medium
* rebuild for Debian 12 Bookworm based release series
-- Proxmox Support Team <support@proxmox.com> Tue, 16 May 2023 14:26:52 +0200
libpve-rs-perl (0.7.6) bullseye; urgency=medium
* update to new tfa crate
* introduce new authentication_verify2 call to utilize the totp/tfa locking
capabilities of the TFA API
-- Proxmox Support Team <support@proxmox.com> Wed, 10 May 2023 10:54:10 +0200
libpve-rs-perl (0.7.5) bullseye; urgency=medium
* update proxmox-resource-scheduling dependency to 0.2.1 to pull in an
improvement for with services where CPU should matter more if there is no
memory load at all
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 17:58:22 +0100
libpve-rs-perl (0.7.4) bullseye; urgency=medium
* initialize logging when shared library is loaded
* update to new TFA crate
* bump proxmox-resource-scheduling dependency to 0.2 to pull in a fix for
usage calculation for homogeneous nodes
* pve: test: resource scheduling: add test with overcommitted node
* update nix to 0.26
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 15:28:08 +0100
libpve-rs-perl (0.7.3) bullseye; urgency=medium
* add PVE::RS::ResourceScheduling::Static and tests

View File

@ -1 +0,0 @@
10

View File

@ -1,39 +1,44 @@
Source: libpve-rs-perl
Section: perl
Priority: optional
Build-Depends:
debhelper (>= 12),
dh-cargo (>= 24),
cargo:native <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-anyhow-1+default-dev,
librust-base32-0.4+default-dev,
librust-base64-0.13+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-nix-0.24+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev,
librust-perlmod-0.13+exporter-dev,
librust-proxmox-apt-0.9+default-dev,
librust-proxmox-http-0.7+client-sync-dev,
librust-proxmox-http-0.7+client-trait-dev,
librust-proxmox-http-0.7+default-dev,
librust-proxmox-openid-0.9+default-dev (>= 0.9.5-~~),
librust-proxmox-resource-scheduling-0.1+default-dev,
librust-proxmox-subscription-0.3+default-dev,
librust-proxmox-sys-0.4+default-dev,
librust-proxmox-tfa-2+api-dev (>= 2.1-~~),
librust-proxmox-tfa-2+default-dev (>= 2.1-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
Build-Depends: cargo:native <!nocheck>,
debhelper-compat (= 13),
dh-cargo (>= 25),
librust-anyhow-1+default-dev,
librust-base32-0.4+default-dev,
librust-base64-0.13+default-dev,
librust-env-logger-0.10+default-dev,
librust-hex-0.4+default-dev,
librust-http-0.2+default-dev (>= 0.2.7-~~),
librust-libc-0.2+default-dev,
librust-log-0.4+default-dev (>= 0.4.17-~~),
librust-nix-0.26+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.40-~~),
librust-perlmod-0.13+default-dev,
librust-perlmod-0.13+exporter-dev,
librust-proxmox-apt-0.10+default-dev (>= 0.10.6-~~),
librust-proxmox-http-0.9+client-sync-dev,
librust-proxmox-http-0.9+client-trait-dev,
librust-proxmox-http-0.9+default-dev,
librust-proxmox-http-error-0.1+default-dev,
librust-proxmox-notify-0.3+default-dev (>= 0.3.1-~~),
librust-proxmox-notify-0.3+pve-context-dev (>= 0.3.1-~~),
librust-proxmox-openid-0.10+default-dev,
librust-proxmox-resource-scheduling-0.3+default-dev,
librust-proxmox-subscription-0.4+default-dev,
librust-proxmox-sys-0.5+default-dev,
librust-proxmox-tfa-4+api-dev (>= 4.0.4-~~),
librust-proxmox-tfa-4+default-dev (>= 4.0.4-~~),
librust-proxmox-time-1+default-dev (>= 1.1.3-~~),
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev,
librust-serde-json-1+default-dev,
librust-url-2+default-dev,
libstd-rust-dev <!nocheck>,
perlmod-bin (>= 0.2.0-3),
rustc:native <!nocheck>,
Maintainer: Proxmox Support Team <support@proxmox.com>
Standards-Version: 4.5.1
Standards-Version: 4.6.1
Vcs-Git: git://git.proxmox.com/git/proxmox-perl-rs.git
Vcs-Browser: https://git.proxmox.com/?p=proxmox-perl-rs.git
Homepage: https://www.proxmox.com
@ -41,14 +46,13 @@ Rules-Requires-Root: no
Package: libpve-rs-perl
Architecture: any
Depends:
${misc:Depends},
${perl:Depends},
${shlibs:Depends},
Breaks:
libpve-access-control (<< 7.1-3),
libpve-common-perl (<< 7.1-4),
pve-manager (<< 7.1-11),
Depends: ${misc:Depends},
${perl:Depends},
${shlibs:Depends},
libproxmox-rs-perl (>= 0.3.3),
Breaks: libpve-access-control (<< 7.1-3),
libpve-common-perl (<< 7.1-4),
pve-manager (<< 7.1-11),
Description: PVE parts which have been ported to Rust - Rust source code
This package contains the source for the Rust pve-rs crate, packaged by
debcargo for use with cargo and dh-cargo.

View File

@ -1,7 +1,25 @@
#!/usr/bin/make -f
include /usr/share/dpkg/pkg-info.mk
include /usr/share/rustc/architecture.mk
#export DH_VERBOSE=1
export BUILD_MODE=release
CARGO=/usr/share/cargo/bin/cargo
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE
export CARGO_HOME = $(CURDIR)/debian/cargo_home
export DEB_CARGO_CRATE=pve-rs_$(DEB_VERSION_UPSTREAM)
export DEB_CARGO_PACKAGE=pve-rs
%:
dh $@
override_dh_auto_configure:
@perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \
die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml
$(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system
dh_auto_configure

View File

@ -1,9 +1,22 @@
//! Rust library for the Proxmox VE code base.
#[path = "../../common/src/mod.rs"]
#[path = "../common/src/mod.rs"]
pub mod common;
//pub mod apt;
pub mod openid;
pub mod resource_scheduling;
pub mod tfa;
#[perlmod::package(name = "Proxmox::Lib::PVE", lib = "pve_rs")]
mod export {
use proxmox_notify::context::pve::PVE_CONTEXT;
use crate::common;
#[export]
pub fn init() {
common::logger::init("PVE_LOG", "info");
proxmox_notify::context::set_context(&PVE_CONTEXT)
}
}

View File

@ -1,6 +1,5 @@
#[perlmod::package(name = "PVE::RS::OpenId", lib = "pve_rs")]
mod export {
use std::convert::TryFrom;
use std::sync::Mutex;
use anyhow::Error;
@ -9,34 +8,13 @@ mod export {
use proxmox_openid::{OpenIdAuthenticator, OpenIdConfig, PrivateAuthState};
const CLASSNAME: &str = "PVE::RS::OpenId";
perlmod::declare_magic!(Box<OpenId> : &OpenId as "PVE::RS::OpenId");
/// An OpenIdAuthenticator client instance.
pub struct OpenId {
inner: Mutex<OpenIdAuthenticator>,
}
impl<'a> TryFrom<&'a Value> for &'a OpenId {
type Error = Error;
fn try_from(value: &'a Value) -> Result<&'a OpenId, Error> {
Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
}
}
fn bless(class: Value, mut ptr: Box<OpenId>) -> Result<Value, Error> {
let value = Value::new_pointer::<OpenId>(&mut *ptr);
let value = Value::new_ref(&value);
let this = value.bless_sv(&class)?;
let _perl = Box::leak(ptr);
Ok(this)
}
#[export(name = "DESTROY")]
fn destroy(#[raw] this: Value) {
perlmod::destructor!(this, OpenId: CLASSNAME);
}
/// Create a new OpenId client instance
#[export(raw_return)]
pub fn discover(
@ -45,12 +23,12 @@ mod export {
redirect_url: &str,
) -> Result<Value, Error> {
let open_id = OpenIdAuthenticator::discover(&config, redirect_url)?;
bless(
class,
Box::new(OpenId {
Ok(perlmod::instantiate_magic!(
&class,
MAGIC => Box::new(OpenId {
inner: Mutex::new(open_id),
}),
)
})
))
}
#[export]

View File

@ -21,11 +21,13 @@ use nix::sys::stat::Mode;
use serde_json::Value as JsonValue;
pub(self) use proxmox_tfa::api::{
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, TfaUserData, U2fConfig, WebauthnConfig,
RecoveryState, TfaChallenge, TfaConfig, TfaResponse, TfaUserData, U2fConfig,
UserChallengeAccess, WebauthnConfig,
};
#[perlmod::package(name = "PVE::RS::TFA")]
mod export {
use std::collections::HashMap;
use std::convert::TryInto;
use std::sync::Mutex;
@ -34,7 +36,7 @@ mod export {
use url::Url;
use perlmod::Value;
use proxmox_tfa::api::methods;
use proxmox_tfa::api::{methods, TfaResult};
use super::{TfaConfig, UserAccess};
@ -173,7 +175,7 @@ mod export {
) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_challenge(UserAccess::new(&raw_this)?, userid, description)
inner.u2f_registration_challenge(&UserAccess::new(&raw_this)?, userid, description)
}
/// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
@ -188,7 +190,7 @@ mod export {
) -> Result<String, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
inner.u2f_registration_finish(UserAccess::new(&raw_this)?, userid, challenge, response)
inner.u2f_registration_finish(&UserAccess::new(&raw_this)?, userid, challenge, response)
}
/// Check if a user has any TFA entries of a given type.
@ -249,7 +251,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?;
let mut inner = this.inner.lock().unwrap();
match inner.authentication_challenge(
UserAccess::new(&raw_this)?,
&UserAccess::new(&raw_this)?,
userid,
origin.as_ref(),
)? {
@ -266,10 +268,7 @@ mod export {
.unwrap()
.users
.get(userid)
.and_then(|user| {
let state = user.recovery_state();
state.is_available().then(move || state)
})
.and_then(|user| user.recovery_state())
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
@ -277,6 +276,8 @@ mod export {
///
/// NOTE: This returns a boolean whether the config data needs to be *saved* after this call
/// (to use up recovery keys!).
///
/// WARNING: This method is now deprecated, as it failures were communicated via croaking.
#[export]
fn authentication_verify(
#[raw] raw_this: Value,
@ -290,15 +291,78 @@ mod export {
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
inner
.verify(
UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
)
.map(|save| save.needs_saving())
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
match result {
TfaResult::Success { needs_saving } => Ok(needs_saving),
_ => bail!("TFA authentication failed"),
}
}
/// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
/// it.
///
/// Returns a result hash of the form:
/// ```text
/// {
/// "result": bool, // whether TFA was successful
/// "needs-saving": bool, // whether the user config needs saving
/// "tfa-limit-reached": bool, // whether the TFA limit was reached (config needs saving)
/// "totp-limit-reached": bool, // whether the TOTP limit was reached (config needs saving)
/// }
/// ```
#[export]
fn authentication_verify2(
#[raw] raw_this: Value,
//#[try_from_ref] this: &Tfa,
userid: &str,
challenge: &str, //super::TfaChallenge,
response: &str,
origin: Option<Url>,
) -> Result<TfaReturnValue, Error> {
let this: &Tfa = (&raw_this).try_into()?;
let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
let response: super::TfaResponse = response.parse()?;
let mut inner = this.inner.lock().unwrap();
let result = inner.verify(
&UserAccess::new(&raw_this)?,
userid,
&challenge,
response,
origin.as_ref(),
);
Ok(match result {
TfaResult::Success { needs_saving } => TfaReturnValue {
result: true,
needs_saving,
..Default::default()
},
TfaResult::Locked => TfaReturnValue::default(),
TfaResult::Failure {
needs_saving,
totp_limit_reached,
tfa_limit_reached,
} => TfaReturnValue {
result: false,
needs_saving,
totp_limit_reached,
tfa_limit_reached,
},
})
}
#[derive(Default, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaReturnValue {
result: bool,
needs_saving: bool,
totp_limit_reached: bool,
tfa_limit_reached: bool,
}
/// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
@ -360,7 +424,7 @@ mod export {
let this: &Tfa = (&raw_this).try_into()?;
methods::add_tfa_entry(
&mut this.inner.lock().unwrap(),
UserAccess::new(&raw_this)?,
&UserAccess::new(&raw_this)?,
userid,
description,
totp,
@ -421,6 +485,66 @@ mod export {
Err(methods::EntryNotFound) => bail!("no such entry"),
}
}
#[export]
fn api_unlock_tfa(#[raw] raw_this: Value, userid: &str) -> Result<bool, Error> {
let this: &Tfa = (&raw_this).try_into()?;
Ok(methods::unlock_and_reset_tfa(
&mut this.inner.lock().unwrap(),
&UserAccess::new(&raw_this)?,
userid,
)?)
}
#[derive(serde::Serialize)]
#[serde(rename_all = "kebab-case")]
struct TfaLockStatus {
/// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use
/// a recovery key.
#[serde(skip_serializing_if = "bool_is_false", default)]
totp_locked: bool,
/// If a user hits too many 2nd factor failures, they get completely blocked for a while.
#[serde(skip_serializing_if = "Option::is_none", default)]
#[serde(deserialize_with = "filter_expired_timestamp")]
tfa_locked_until: Option<i64>,
}
impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus {
fn from(data: &proxmox_tfa::api::TfaUserData) -> Self {
Self {
totp_locked: data.totp_locked,
tfa_locked_until: data.tfa_locked_until,
}
}
}
fn bool_is_false(b: &bool) -> bool {
!*b
}
#[export]
fn tfa_lock_status(
#[try_from_ref] this: &Tfa,
userid: Option<&str>,
) -> Result<Option<perlmod::Value>, Error> {
let this = this.inner.lock().unwrap();
if let Some(userid) = userid {
if let Some(user) = this.users.get(userid) {
Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?))
} else {
Ok(None)
}
} else {
Ok(Some(perlmod::to_value(
&HashMap::<String, TfaLockStatus>::from_iter(
this.users
.iter()
.map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))),
),
)?))
}
}
}
/// Version 1 format of `/etc/pve/priv/tfa.cfg`
@ -514,6 +638,7 @@ fn decode_old_entry(ty: &[u8], data: &[u8], user: &str) -> Result<TfaUserData, E
b"oath" => user_data.totp.extend(
decode_old_oath_entry(value, user)?
.into_iter()
.map(proxmox_tfa::api::TotpEntry::new)
.map(move |entry| proxmox_tfa::api::TfaEntry::from_parts(info.clone(), entry)),
),
b"yubico" => user_data.yubico.extend(
@ -841,9 +966,7 @@ fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
}
impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
type Data = UserChallengeData;
fn open(&self, userid: &str) -> Result<UserChallengeData, Error> {
fn open(&self, userid: &str) -> Result<Box<dyn UserChallengeAccess>, Error> {
if self.is_debug() {
mkdir("./local-tfa-challenges", 0o700)?;
} else {
@ -886,15 +1009,15 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
}
};
Ok(UserChallengeData {
Ok(Box::new(UserChallengeData {
inner,
path,
lock: file,
})
}))
}
/// `open` without creating the file if it doesn't exist, to finish WA authentications.
fn open_no_create(&self, userid: &str) -> Result<Option<UserChallengeData>, Error> {
fn open_no_create(&self, userid: &str) -> Result<Option<Box<dyn UserChallengeAccess>>, Error> {
let path = challenge_data_path(userid, self.is_debug());
let mut file = match std::fs::OpenOptions::new()
@ -915,11 +1038,11 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
format_err!("failed to read challenge data for user {}: {}", userid, err)
})?;
Ok(Some(UserChallengeData {
Ok(Some(Box::new(UserChallengeData {
inner,
path,
lock: file,
}))
})))
}
fn remove(&self, userid: &str) -> Result<bool, Error> {
@ -930,6 +1053,10 @@ impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
Err(err) => Err(err.into()),
}
}
fn enable_lockout(&self) -> bool {
true
}
}
/// Container of `TfaUserChallenges` with the corresponding file lock guard.
@ -947,7 +1074,7 @@ impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
&mut self.inner
}
fn save(self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
UserChallengeData::save(self)
}
}
@ -992,7 +1119,7 @@ impl UserChallengeData {
///
/// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
/// way also unlocks early.
fn save(mut self) -> Result<(), Error> {
fn save(&mut self) -> Result<(), Error> {
self.rewind()?;
serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {

View File

@ -1,4 +1,9 @@
.PHONY: test
test:
test: Proxmox/Lib/PVE.pm
@echo "-- running pve-rs tests --"
./resource_scheduling.pl
perl -I. -I.. -I../.. ./resource_scheduling.pl
# The test stub, we don't know where to look for the library from in here!
Proxmox/Lib/PVE.pm:
@echo "run 'make' in the pve-rs/ dir first"
@exit 1

View File

@ -5,35 +5,73 @@ use warnings;
use Test::More;
# FIXME ensure that the just built library is loaded rather than the installed one and add a test
# target to pve-rs/Makefile afterwards. Issue is that the loader looks into an $PATH/auto directory,
# so it's not enough to use lib qw(../target/release)
# Also might be a good idea to test for existence of the files to avoid surprises if the directory
# structure changes in the future.
#use lib qw(..);
#use lib qw(../target/release);
use PVE::RS::ResourceScheduling::Static;
my $static = PVE::RS::ResourceScheduling::Static->new();
is(scalar($static->list_nodes()->@*), 0, 'node list empty');
$static->add_node("A", 10, 100_000_000_000);
is(scalar($static->list_nodes()->@*), 1, '1 node added');
$static->add_node("B", 20, 200_000_000_000);
is(scalar($static->list_nodes()->@*), 2, '2nd node');
$static->add_node("C", 30, 300_000_000_000);
is(scalar($static->list_nodes()->@*), 3, '3rd node');
$static->remove_node("C");
is(scalar($static->list_nodes()->@*), 2, '3rd removed should be 2');
ok($static->contains_node("A"), 'should contain a node A');
ok($static->contains_node("B"), 'should contain a node B');
ok(!$static->contains_node("C"), 'should not contain a node C');
sub test_basic {
my $static = PVE::RS::ResourceScheduling::Static->new();
is(scalar($static->list_nodes()->@*), 0, 'node list empty');
$static->add_node("A", 10, 100_000_000_000);
is(scalar($static->list_nodes()->@*), 1, '1 node added');
$static->add_node("B", 20, 200_000_000_000);
is(scalar($static->list_nodes()->@*), 2, '2nd node');
$static->add_node("C", 30, 300_000_000_000);
is(scalar($static->list_nodes()->@*), 3, '3rd node');
$static->remove_node("C");
is(scalar($static->list_nodes()->@*), 2, '3rd removed should be 2');
ok($static->contains_node("A"), 'should contain a node A');
ok($static->contains_node("B"), 'should contain a node B');
ok(!$static->contains_node("C"), 'should not contain a node C');
}
my $service = {
maxcpu => 4,
maxmem => 20_000_000_000,
};
sub test_balance {
my $static = PVE::RS::ResourceScheduling::Static->new();
$static->add_node("A", 10, 100_000_000_000);
$static->add_node("B", 20, 200_000_000_000);
my $service = {
maxcpu => 4,
maxmem => 20_000_000_000,
};
for (my $i = 0; $i < 15; $i++) {
my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager
my $scores = { map { $_->[0] => -$_->[1] } $score_list->@* };
my @nodes = sort {
$scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*;
if ($i % 3 == 2) {
is($nodes[0], "A", 'first should be A');
is($nodes[1], "B", 'second should be A');
} else {
is($nodes[0], "B", 'first should be B');
is($nodes[1], "A", 'second should be A');
}
$static->add_service_usage_to_node($nodes[0], $service);
}
}
sub test_overcommitted {
my $static = PVE::RS::ResourceScheduling::Static->new();
$static->add_node("A", 4, 4_102_062_080);
$static->add_node("B", 4, 4_102_062_080);
$static->add_node("C", 4, 4_102_053_888);
$static->add_node("D", 4, 4_102_053_888);
my $service = {
maxcpu => 1,
maxmem => 536_870_912,
};
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("A", $service);
$static->add_service_usage_to_node("B", $service);
$static->add_service_usage_to_node("A", $service);
for (my $i = 0; $i < 15; $i++) {
my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager
@ -42,15 +80,65 @@ for (my $i = 0; $i < 15; $i++) {
$scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*;
if ($i % 3 == 2) {
is($nodes[0], "A", 'first should be A');
is($nodes[1], "B", 'second should be A');
} else {
is($nodes[0], "B", 'first should be B');
is($nodes[1], "A", 'second should be A');
}
$static->add_service_usage_to_node($nodes[0], $service);
is($nodes[0], "C", 'first should be C');
is($nodes[1], "D", 'second should be D');
is($nodes[2], "B", 'third should be B');
is($nodes[3], "A", 'fourth should be A');
}
sub test_balance_small_memory_difference {
my ($with_start_load) = @_;
my $static = PVE::RS::ResourceScheduling::Static->new();
# Memory is different to avoid flaky results with what would otherwise be ties.
$static->add_node("A", 8, 10_000_000_000);
$static->add_node("B", 4, 9_000_000_000);
$static->add_node("C", 4, 8_000_000_000);
if ($with_start_load) {
$static->add_service_usage_to_node("A", { maxcpu => 4, maxmem => 1_000_000_000 });
$static->add_service_usage_to_node("B", { maxcpu => 2, maxmem => 1_000_000_000 });
$static->add_service_usage_to_node("C", { maxcpu => 2, maxmem => 1_000_000_000 });
}
my $service = {
maxcpu => 3,
maxmem => 16_000_000,
};
for (my $i = 0; $i < 20; $i++) {
my $score_list = $static->score_nodes_to_start_service($service);
# imitate HA manager
my $scores = { map { $_->[0] => -$_->[1] } $score_list->@* };
my @nodes = sort {
$scores->{$a} <=> $scores->{$b} || $a cmp $b
} keys $scores->%*;
if ($i % 4 <= 1) {
is($nodes[0], "A", 'first should be A');
is($nodes[1], "B", 'second should be B');
is($nodes[2], "C", 'third should be C');
} elsif ($i % 4 == 2) {
is($nodes[0], "B", 'first should be B');
is($nodes[1], "C", 'second should be C');
is($nodes[2], "A", 'third should be A');
} elsif ($i % 4 == 3) {
is($nodes[0], "C", 'first should be C');
is($nodes[1], "A", 'second should be A');
is($nodes[2], "B", 'third should be B');
} else {
die "internal error, got $i % 4 == " . ($i % 4) . "\n";
}
$static->add_service_usage_to_node($nodes[0], $service);
}
}
test_basic();
test_balance();
test_overcommitted();
test_balance_small_memory_difference(1);
test_balance_small_memory_difference(0);
done_testing();

View File

@ -1,32 +0,0 @@
#!/usr/bin/env perl
# Create a perl package given a product and package name.
use strict;
use warnings;
use File::Path qw(make_path);
my $product = shift @ARGV or die "missing product name (PVE, PMG or Common)\n";
die "missing package name\n" if !@ARGV;
for my $package (@ARGV) {
my $path = ($package =~ s@::@/@gr) . ".pm";
print "Generating $path\n";
$path =~ m@^(.*)/[^/]+@;
make_path($1, { mode => 0755 });
open(my $fh, '>', $path) or die "failed to open '$path' for writing: $!\n";
print {$fh} <<"EOF";
package $package;
use base 'Proxmox::Lib::$product';
BEGIN { __PACKAGE__->bootstrap(); }
1;
EOF
close($fh);
}