Merge 8.2.4

This commit is contained in:
Andrew A. Vasilyev 2024-08-29 23:26:17 +03:00
commit d5867bc8a1
73 changed files with 1723 additions and 414 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ dest/
/www/mobile/pvemanager-mobile.js
/www/touch/touch-[0-9]*/
/pve-manager-[0-9]*/
/test/.mocked_*
/test/*.tmp

View File

@ -10,7 +10,7 @@ DSC=$(PACKAGE)_$(DEB_VERSION).dsc
DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
DESTDIR=
SUBDIRS = aplinfo PVE bin www services configs network-hooks test
SUBDIRS = aplinfo PVE bin www services configs network-hooks test templates
all: $(SUBDIRS)
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done

View File

@ -238,12 +238,6 @@ __PACKAGE__->register_method({
return $pkglist;
}});
my $updates_available_subject_template = "New software packages available ({{hostname}})";
my $updates_available_body_template = <<EOT;
The following updates are available:
{{table updates}}
EOT
__PACKAGE__->register_method({
name => 'update_database',
path => 'update',
@ -358,8 +352,7 @@ __PACKAGE__->register_method({
};
PVE::Notify::info(
$updates_available_subject_template,
$updates_available_body_template,
"package-updates",
$template_data,
$metadata_fields,
);
@ -759,6 +752,7 @@ __PACKAGE__->register_method({
push @list, sort $byver grep { /^(?:pve|proxmox)-kernel-/ && $cache->{$_}->{CurrentState} eq 'Installed' } keys %$cache;
my @opt_pack = qw(
amd64-microcode
ceph
criu
dnsmasq
@ -770,6 +764,7 @@ __PACKAGE__->register_method({
libpve-network-perl
openvswitch
proxmox-backup-file-restore
pve-firewall
pve-zsync
zfs-utils
);

View File

@ -56,7 +56,7 @@ my $vzdump_job_id_prop = {
# NOTE: also used by the vzdump API call.
sub assert_param_permission_common {
my ($rpcenv, $user, $param) = @_;
my ($rpcenv, $user, $param, $is_delete) = @_;
return if $user eq 'root@pam'; # always OK
for my $key (qw(tmpdir dumpdir script)) {
@ -66,6 +66,12 @@ sub assert_param_permission_common {
if (grep { defined($param->{$_}) } qw(bwlimit ionice performance)) {
$rpcenv->check($user, "/", [ 'Sys.Modify' ]);
}
if ($param->{fleecing} && !$is_delete) {
my $fleecing = PVE::VZDump::parse_fleecing($param) // {};
$rpcenv->check($user, "/storage/$fleecing->{storage}", [ 'Datastore.AllocateSpace' ])
if $fleecing->{storage};
}
}
my sub assert_param_permission_create {
@ -84,7 +90,7 @@ my sub assert_param_permission_update {
return if $user eq 'root@pam'; # always OK
assert_param_permission_common($rpcenv, $user, $update);
assert_param_permission_common($rpcenv, $user, $delete);
assert_param_permission_common($rpcenv, $user, $delete, 1);
if ($update->{storage}) {
$rpcenv->check($user, "/storage/$update->{storage}", [ 'Datastore.Allocate' ])

View File

@ -192,6 +192,11 @@ __PACKAGE__->register_method ({
PVE::Ceph::Tools::check_ceph_installed('ceph_bin');
}
my $pve_ceph_cfgdir = PVE::Ceph::Tools::get_config('pve_ceph_cfgdir');
if (! -d $pve_ceph_cfgdir) {
File::Path::make_path($pve_ceph_cfgdir);
}
my $auth = $param->{disable_cephx} ? 'none' : 'cephx';
# simply load old config if it already exists

View File

@ -451,6 +451,14 @@ __PACKAGE__->register_method ({
)
};
warn "$@" if $@;
print "Configuring keyring for ceph-crash.service\n";
eval {
PVE::Ceph::Tools::create_or_update_crash_keyring_file();
$cfg->{'client.crash'}->{keyring} = '/etc/pve/ceph/$cluster.$name.keyring';
cfs_write_file('ceph.conf', $cfg);
};
warn "Unable to configure keyring for ceph-crash.service: $@" if $@;
}
eval { PVE::Ceph::Services::ceph_service_cmd('enable', $monsection) };

View File

@ -107,6 +107,7 @@ __PACKAGE__->register_method ({
my $result = [
{ name => 'gotify' },
{ name => 'sendmail' },
{ name => 'smtp' },
];
return $result;
@ -143,7 +144,7 @@ __PACKAGE__->register_method ({
'type' => {
description => 'Type of the target.',
type => 'string',
enum => [qw(sendmail gotify)],
enum => [qw(sendmail gotify smtp)],
},
'comment' => {
description => 'Comment',

View File

@ -1017,7 +1017,8 @@ my $get_vnc_connection_info = sub {
my ($remip, $family);
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
($remip, $family) = PVE::Cluster::remote_node_ip($node);
$remote_cmd = ['/usr/bin/ssh', '-e', 'none', '-t', $remip , '--'];
$remote_cmd = PVE::SSHInfo::ssh_info_to_command({ ip => $remip, name => $node }, ('-t'));
push @$remote_cmd, '--';
} else {
$family = PVE::Tools::get_host_address_family($node);
}

View File

@ -92,23 +92,6 @@ my sub _should_mail_at_failcount {
return $i * 48 == $fail_count;
};
my $replication_error_subject_template = "Replication Job: '{{job-id}}' failed";
my $replication_error_body_template = <<EOT;
{{#verbatim}}
Replication job '{{job-id}}' with target '{{job-target}}' and schedule '{{job-schedule}}' failed!
Last successful sync: {{timestamp last-sync}}
Next sync try: {{timestamp next-sync}}
Failure count: {{failure-count}}
{{#if (eq failure-count 3)}}
Note: The system will now reduce the frequency of error reports, as the job
appears to be stuck.
{{/if}}
Error:
{{verbatim-monospaced error}}
{{/verbatim}}
EOT
my sub _handle_job_err {
my ($job, $err, $mail) = @_;
@ -146,8 +129,7 @@ my sub _handle_job_err {
eval {
PVE::Notify::error(
$replication_error_subject_template,
$replication_error_body_template,
"replication",
$template_data,
$metadata_fields
);

View File

@ -41,10 +41,11 @@ __PACKAGE__->register_method ({
description => "Create backup.",
permissions => {
description => "The user needs 'VM.Backup' permissions on any VM, and "
."'Datastore.AllocateSpace' on the backup storage. The 'tmpdir', 'dumpdir' and "
."'script' parameters are restricted to the 'root\@pam' user. The 'maxfiles' and "
."'prune-backups' settings require 'Datastore.Allocate' on the backup storage. The "
."'bwlimit', 'performance' and 'ionice' parameters require 'Sys.Modify' on '/'. ",
."'Datastore.AllocateSpace' on the backup storage (and fleecing storage when fleecing "
."is used). The 'tmpdir', 'dumpdir' and 'script' parameters are restricted to the "
."'root\@pam' user. The 'maxfiles' and 'prune-backups' settings require "
."'Datastore.Allocate' on the backup storage. The 'bwlimit', 'performance' and "
."'ionice' parameters require 'Sys.Modify' on '/'.",
user => 'all',
},
protected => 1,

View File

@ -116,7 +116,7 @@ sub proxy_handler {
}
}
my @ssh_tunnel_cmd = ('ssh', '-o', 'BatchMode=yes', "root\@$remip");
my $ssh_tunnel_cmd = PVE::SSHInfo::ssh_info_to_command({ ip => $remip, name => $node });
my @pvesh_cmd = ('pvesh', '--noproxy', $cmd, $path, '--output-format', 'json');
if (scalar(@$args)) {
@ -126,7 +126,7 @@ sub proxy_handler {
my $res = '';
PVE::Tools::run_command(
[ @ssh_tunnel_cmd, '--', @pvesh_cmd ],
[ $ssh_tunnel_cmd->@*, '--', @pvesh_cmd ],
errmsg => "proxy handler failed",
outfunc => sub { $res .= shift },
);

View File

@ -18,7 +18,9 @@ my $ccname = 'ceph'; # ceph cluster name
my $ceph_cfgdir = "/etc/ceph";
my $pve_ceph_cfgpath = "/etc/pve/$ccname.conf";
my $ceph_cfgpath = "$ceph_cfgdir/$ccname.conf";
my $pve_ceph_cfgdir = "/etc/pve/ceph";
my $pve_ceph_crash_key_path = "$pve_ceph_cfgdir/$ccname.client.crash.keyring";
my $pve_mon_key_path = "/etc/pve/priv/$ccname.mon.keyring";
my $pve_ckeyring_path = "/etc/pve/priv/$ccname.client.admin.keyring";
my $ckeyring_path = "/etc/ceph/ceph.client.admin.keyring";
@ -37,12 +39,14 @@ my $ceph_service = {
my $config_values = {
ccname => $ccname,
pve_ceph_cfgdir => $pve_ceph_cfgdir,
ceph_mds_data_dir => $ceph_mds_data_dir,
long_rados_timeout => 60,
};
my $config_files = {
pve_ceph_cfgpath => $pve_ceph_cfgpath,
pve_ceph_crash_key_path => $pve_ceph_crash_key_path,
pve_mon_key_path => $pve_mon_key_path,
pve_ckeyring_path => $pve_ckeyring_path,
ceph_bootstrap_osd_keyring => $ceph_bootstrap_osd_keyring,
@ -186,8 +190,14 @@ sub check_ceph_inited {
return undef if !check_ceph_installed('ceph_mon', $noerr);
if (! -f $pve_ceph_cfgpath) {
die "pveceph configuration not initialized\n" if !$noerr;
my @errors;
push(@errors, "missing '$pve_ceph_cfgpath'") if ! -f $pve_ceph_cfgpath;
push(@errors, "missing '$pve_ceph_cfgdir'") if ! -d $pve_ceph_cfgdir;
if (@errors) {
my $err = 'pveceph configuration not initialized - ' . join(', ', @errors) . "\n";
die $err if !$noerr;
return undef;
}
@ -412,6 +422,39 @@ sub get_or_create_admin_keyring {
return $pve_ckeyring_path;
}
# is also used in `pve-init-ceph-crash` helper
sub create_or_update_crash_keyring_file {
my ($rados) = @_;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
}
my $output = $rados->mon_command({
prefix => 'auth get-or-create',
entity => 'client.crash',
caps => [
mon => 'profile crash',
mgr => 'profile crash',
],
format => 'plain',
});
if (-f $pve_ceph_crash_key_path) {
my $contents = PVE::Tools::file_get_contents($pve_ceph_crash_key_path);
if ($contents ne $output) {
PVE::Tools::file_set_contents($pve_ceph_crash_key_path, $output);
return 1;
}
} else {
PVE::Tools::file_set_contents($pve_ceph_crash_key_path, $output);
return 1;
}
return 0;
}
# get ceph-volume managed osds
sub ceph_volume_list {
my $result = {};

View File

@ -31,12 +31,15 @@ my $init_report_cmds = sub {
cmds => [
'hostname',
'date -R',
'cat /proc/cmdline',
'pveversion --verbose',
'cat /etc/hosts',
'pvesubscription get',
'cat /etc/apt/sources.list',
sub { dir2text('/etc/apt/sources.list.d/', '.*list') },
sub { dir2text('/etc/apt/sources.list.d/', '.*sources') },
sub { dir2text('/etc/apt/sources.list.d/', '.+\.list') },
sub { dir2text('/etc/apt/sources.list.d/', '.+\.sources') },
'apt-cache policy | grep -vP "^ +origin "',
'apt-mark showhold',
'lscpu',
'pvesh get /cluster/resources --type node --output-format=yaml',
],
@ -64,9 +67,9 @@ my $init_report_cmds = sub {
order => 40,
cmds => [
'qm list',
sub { dir2text('/etc/pve/qemu-server/', '\d.*conf') },
sub { dir2text('/etc/pve/qemu-server/', '\d+\.conf') },
'pct list',
sub { dir2text('/etc/pve/lxc/', '\d.*conf') },
sub { dir2text('/etc/pve/lxc/', '\d+\.conf') },
],
},
network => {
@ -83,7 +86,7 @@ my $init_report_cmds = sub {
firewall => {
order => 50,
cmds => [
sub { dir2text('/etc/pve/firewall/', '.*fw') },
sub { dir2text('/etc/pve/firewall/', '.+\.fw') },
'cat /etc/pve/local/host.fw',
'iptables-save -c | column -t -l4 -o" "',
],
@ -98,6 +101,12 @@ my $init_report_cmds = sub {
'cat /etc/pve/datacenter.cfg',
],
},
jobs => {
order => 65,
cmds => [
'cat /etc/pve/jobs.cfg',
],
},
hardware => {
order => 70,
cmds => [

View File

@ -677,8 +677,3 @@ our $cmddef = {
};
1;

View File

@ -130,13 +130,37 @@ my $generate_notes = sub {
return $notes_template;
};
sub parse_fleecing {
my ($param) = @_;
if (defined(my $fleecing = $param->{fleecing})) {
return $fleecing if ref($fleecing) eq 'HASH'; # already parsed
$param->{fleecing} = PVE::JSONSchema::parse_property_string('backup-fleecing', $fleecing);
}
return $param->{fleecing};
}
my sub parse_performance {
my ($param) = @_;
if (defined(my $perf = $param->{performance})) {
return if ref($perf) eq 'HASH'; # already parsed
return $perf if ref($perf) eq 'HASH'; # already parsed
$param->{performance} = PVE::JSONSchema::parse_property_string('backup-performance', $perf);
}
return $param->{performance};
}
my sub merge_performance {
my ($prefer, $fallback) = @_;
my $res = {};
for my $opt (keys PVE::JSONSchema::get_format('backup-performance')->%*) {
$res->{$opt} = $prefer->{$opt} // $fallback->{$opt}
if defined($prefer->{$opt}) || defined($fallback->{$opt});
}
return $res;
}
my $parse_prune_backups_maxfiles = sub {
@ -149,7 +173,7 @@ my $parse_prune_backups_maxfiles = sub {
if defined($maxfiles) && defined($prune_backups);
if (defined($prune_backups)) {
return if ref($prune_backups) eq 'HASH'; # already parsed
return $prune_backups if ref($prune_backups) eq 'HASH'; # already parsed
$param->{'prune-backups'} = PVE::JSONSchema::parse_property_string(
'prune-backups',
$prune_backups
@ -161,6 +185,8 @@ my $parse_prune_backups_maxfiles = sub {
$param->{'prune-backups'} = { 'keep-all' => 1 };
}
}
return $param->{'prune-backups'};
};
sub storage_info {
@ -277,8 +303,21 @@ sub read_vzdump_defaults {
defined($default) ? ($_ => $default) : ()
} keys %$confdesc_for_defaults
};
my $performance_fmt = PVE::JSONSchema::get_format('backup-performance');
$defaults->{performance} = {
map {
my $default = $performance_fmt->{$_}->{default};
defined($default) ? ($_ => $default) : ()
} keys $performance_fmt->%*
};
my $fleecing_fmt = PVE::JSONSchema::get_format('backup-fleecing');
$defaults->{fleecing} = {
map {
my $default = $fleecing_fmt->{$_}->{default};
defined($default) ? ($_ => $default) : ()
} keys $fleecing_fmt->%*
};
$parse_prune_backups_maxfiles->($defaults, "defaults in VZDump schema");
parse_performance($defaults);
my $raw;
eval { $raw = PVE::Tools::file_get_contents($fn); };
@ -304,10 +343,15 @@ sub read_vzdump_defaults {
$res->{mailto} = [ @mailto ];
}
$parse_prune_backups_maxfiles->($res, "options in '$fn'");
parse_fleecing($res);
parse_performance($res);
foreach my $key (keys %$defaults) {
$res->{$key} = $defaults->{$key} if !defined($res->{$key});
for my $key (keys $defaults->%*) {
if (!defined($res->{$key})) {
$res->{$key} = $defaults->{$key};
} elsif ($key eq 'performance') {
$res->{$key} = merge_performance($res->{$key}, $defaults->{$key});
}
}
if (defined($res->{storage}) && defined($res->{dumpdir})) {
@ -433,20 +477,6 @@ my sub get_hostname {
return $hostname;
}
my $subject_template = "vzdump backup status ({{hostname}}): {{status-text}}";
my $body_template = <<EOT;
{{error-message}}
{{heading-1 "Details"}}
{{table guest-table}}
{{#verbatim}}
Total running time: {{duration total-time}}
Total size: {{human-bytes total-size}}
{{/verbatim}}
{{heading-1 "Logs"}}
{{verbatim-monospaced logs}}
EOT
use constant MAX_LOG_SIZE => 1024*1024;
sub send_notification {
@ -544,8 +574,7 @@ sub send_notification {
PVE::Notify::notify(
$severity,
$subject_template,
$body_template,
"vzdump",
$notification_props,
$fields,
$notification_config
@ -556,8 +585,7 @@ sub send_notification {
# no email addresses were configured.
PVE::Notify::notify(
$severity,
$subject_template,
$body_template,
"vzdump",
$notification_props,
$fields,
);
@ -592,8 +620,10 @@ sub new {
if ($k eq 'dumpdir' || $k eq 'storage') {
$opts->{$k} = $defaults->{$k} if !defined ($opts->{dumpdir}) &&
!defined ($opts->{storage});
} else {
$opts->{$k} = $defaults->{$k} if !defined ($opts->{$k});
} elsif (!defined($opts->{$k})) {
$opts->{$k} = $defaults->{$k};
} elsif ($k eq 'performance') {
$opts->{$k} = merge_performance($opts->{$k}, $defaults->{$k});
}
}
@ -1273,6 +1303,7 @@ sub exec_backup_task {
debugmsg ('info', "Failed at " . strftime("%F %H:%M:%S", localtime()));
eval { $self->run_hook_script ('backup-abort', $task, $logfd); };
debugmsg('warn', $@) if $@; # message already contains command with phase name
} else {
$task->{state} = 'ok';
@ -1304,6 +1335,7 @@ sub exec_backup_task {
}
eval { $self->run_hook_script ('log-end', $task); };
debugmsg('warn', $@) if $@; # message already contains command with phase name
die $err if $err && $err =~ m/^interrupted by signal$/;
}
@ -1453,6 +1485,7 @@ sub verify_vzdump_parameters {
if defined($param->{'prune-backups'}) && defined($param->{maxfiles});
$parse_prune_backups_maxfiles->($param, 'CLI parameters');
parse_fleecing($param);
parse_performance($param);
if (my $template = $param->{'notes-template'}) {

View File

@ -30,6 +30,19 @@ sha512sum: f9d19b4a6d3c6201cf7a4624baf2d16ef08e7668adec0b7ea1a9c0ab8d854c8a9d11d
Infopage: https://linuxcontainers.org
Description: LXC default image for alpine 3.18 (20230607)
Package: alpine-3.19-default
Version: 20240207
Type: lxc
OS: alpine
Section: system
Maintainer: Proxmox Support Team <support@proxmox.com>
Architecture: amd64
Location: system/alpine-3.19-default_20240207_amd64.tar.xz
md5sum: fe35133232231ed5918c3828e91c4069
sha512sum: dec171b608802827a2b47ae6c473f71fdea5fae0064a7928749fc5a854a7220a3244e405fb5ad14d09f52e5325a399c903169a075cd2f6787326972094561f0a
Infopage: https://linuxcontainers.org
Description: LXC default image for alpine 3.19 (20240207)
Package: archlinux-base
Version: 20230608-1
Type: lxc
@ -164,6 +177,20 @@ sha512sum: f3a6785c347da3867d074345b68db9c99ec2b269e454f715d234935014ca1dc9f7239
Infopage: https://linuxcontainers.org
Description: LXC default image for opensuse 15.5 (20231118)
Package: proxmox-mail-gateway-8.1-standard
Version: 8.1-1
Type: lxc
OS: debian-12
Section: mail
Maintainer: Proxmox Support Team <support@proxmox.com>
Architecture: amd64
Location: mail/proxmox-mail-gateway-8.1-standard_8.1-1_amd64.tar.zst
md5sum: f227d87298985adf3e8aa1e946437a0e
sha512sum: 9739d43874faaa8670e8b0cc593476e6a4bc49f8f641e2ef5b6b24366b2df34d752d98be87c4ffc22d5a3d08a75414e708a7769a1c3054ef60a90323b032a50b
Infopage: https://www.proxmox.com/en/proxmox-mail-gateway/overview
Description: Proxmox Mail Gateway 8.1
A full featured mail proxy for spam and virus filtering, optimized for container environment.
Package: proxmox-mailgateway-7.3-standard
Version: 7.3-1
Type: lxc
@ -178,20 +205,6 @@ Infopage: https://www.proxmox.com/de/proxmox-mail-gateway
Description: Proxmox Mailgateway 7.3
A full featured mail proxy for spam and virus filtering, optimized for container environment.
Package: proxmox-mailgateway-8.0-standard
Version: 8.0-1
Type: lxc
OS: debian-12
Section: mail
Maintainer: Proxmox Support Team <support@proxmox.com>
Architecture: amd64
Location: mail/proxmox-mailgateway-8.0-standard_8.0-1_amd64.tar.zst
md5sum: 7d321e5dfc6e1005231586d1871e3625
sha512sum: be5efcb8ee97f2bb1c638360191eda19f49e2063acb88da55c948c90c091063972cc9ea29e6aeaa4a85733e0fb2c99ea905d665ac693cb2bf06b091c4baf781f
Infopage: https://www.proxmox.com/de/proxmox-mail-gateway
Description: Proxmox Mailgateway 8.0
A full featured mail proxy for spam and virus filtering, optimized for container environment.
Package: rockylinux-9-default
Version: 20221109
Type: lxc
@ -260,3 +273,17 @@ sha512sum: 84bcb7348ba86026176ed35e5b798f89cb64b0bcbe27081e3f97e3002a6ec34d836bb
Infopage: https://pve.proxmox.com/wiki/Linux_Container#pct_supported_distributions
Description: Ubuntu 23.10 Mantic (standard)
A small Ubuntu 23.10 Mantic Minotaur system including all standard packages.
Package: ubuntu-24.04-standard
Version: 24.04-2
Type: lxc
OS: ubuntu-24.04
Section: system
Maintainer: Proxmox Support Team <support@proxmox.com>
Architecture: amd64
Location: system/ubuntu-24.04-standard_24.04-2_amd64.tar.zst
md5sum: 4030982618eeae70854e8f9711adbd09
sha512sum: 45c2978e6b97fe292ada95fe06834276015e5739a594db4de2fdfd830fa0c37942e8ae118fc1e32ffd9154b3f9378b592738b668ea3957db41f2907b86f219de
Infopage: https://pve.proxmox.com/wiki/Linux_Container#pct_supported_distributions
Description: Ubuntu 24.04 Noble (standard)
A small Ubuntu 24.04 Noble Numbat system including all standard packages.

View File

@ -25,6 +25,10 @@ SCRIPTS = \
pveperf \
pvereport
HELPERS = \
pve-startall-delay \
pve-init-ceph-crash
SERVICE_MANS = $(addsuffix .8, $(SERVICES))
CLI_MANS = \
@ -82,7 +86,7 @@ install: $(SCRIPTS) $(CLI_MANS) $(SERVICE_MANS) $(BASH_COMPLETIONS) $(ZSH_COMPLE
install -d $(BINDIR)
install -m 0755 $(SCRIPTS) $(BINDIR)
install -d $(USRSHARE)/helpers
install -m 0755 pve-startall-delay $(USRSHARE)/helpers
install -m 0755 $(HELPERS) $(USRSHARE)/helpers
install -d $(MAN1DIR)
install -m 0644 $(CLI_MANS) $(MAN1DIR)
install -d $(MAN8DIR)

150
bin/pve-init-ceph-crash Executable file
View File

@ -0,0 +1,150 @@
#!/usr/bin/perl
use strict;
use warnings;
use List::Util qw(first);
use PVE::Ceph::Tools;
use PVE::Cluster;
use PVE::RADOS;
use PVE::RPCEnvironment;
my $ceph_cfg_file = 'ceph.conf';
my $keyring_value = '/etc/pve/ceph/$cluster.$name.keyring';
sub try_adapt_cfg {
my ($cfg) = @_;
my $entity = 'client.crash';
my $removed_key = 0;
print("Checking whether the configuration for '$entity' needs to be updated.\n");
my $add_keyring = sub {
print("Setting keyring path to '$keyring_value'.\n");
$cfg->{$entity}->{keyring} = $keyring_value;
};
if (!exists($cfg->{$entity})) {
print("Adding missing section for '$entity'.\n");
$add_keyring->();
return 1;
}
if (exists($cfg->{$entity}->{key})) {
print("Removing existing usage of key.\n");
delete($cfg->{$entity}->{key});
$removed_key = 1;
}
if (!exists($cfg->{$entity}->{keyring})) {
print("Keyring path is missing from configuration.\n");
$add_keyring->();
return 1;
}
my $current_keyring_value = $cfg->{$entity}->{keyring};
if ($current_keyring_value ne $keyring_value) {
print("Current keyring path differs from expected path.\n");
$add_keyring->();
return 1;
}
return $removed_key;
}
sub main {
# PVE::RADOS expects an active RPC Environment because it forks itself
# and may want to clean up after
my $rpcenv = PVE::RPCEnvironment->setup_default_cli_env();
if (!PVE::Ceph::Tools::check_ceph_installed('ceph_bin', 1)) {
print("Ceph is not installed. No action required.\n");
exit 0;
}
my $ceph_cfg_path = PVE::Ceph::Tools::get_config('pve_ceph_cfgpath');
if (PVE::Ceph::Tools::check_ceph_installed('ceph_mon', 1) && -f $ceph_cfg_path) {
my $pve_ceph_cfgdir = PVE::Ceph::Tools::get_config('pve_ceph_cfgdir');
if (! -d $pve_ceph_cfgdir) {
File::Path::make_path($pve_ceph_cfgdir);
}
}
eval {
PVE::Ceph::Tools::check_ceph_inited();
};
if ($@) {
print("Ceph is not initialized. No action required.\n");
exit 0;
}
my $rados = eval { PVE::RADOS->new() };
my $ceph_crash_key_path = PVE::Ceph::Tools::get_config('pve_ceph_crash_key_path');
my $inner_err = '';
my $rval = PVE::Cluster::cfs_lock_file($ceph_cfg_file, undef, sub {
eval {
my $cfg = PVE::Cluster::cfs_read_file($ceph_cfg_file);
if (!defined($rados)) {
my $has_mon_host = defined($cfg->{global}) && defined($cfg->{global}->{mon_host});
if ($has_mon_host && $cfg->{global}->{mon_host} ne '') {
die "Connection to RADOS failed even though a monitor is configured.\n" .
"Please verify whether your configuration in '$ceph_cfg_file' is correct.\n"
}
print(
"Connection to RADOS failed and no monitor is configured in '$ceph_cfg_file'.\n".
"Assuming that things are fine. No action required.\n"
);
return;
}
my $updated_keyring = PVE::Ceph::Tools::create_or_update_crash_keyring_file($rados);
if ($updated_keyring) {
print("Keyring file '$ceph_crash_key_path' was updated.\n");
}
my $changed = try_adapt_cfg($cfg);
if ($changed) {
print("Committing updated configuration to '$ceph_cfg_file'.\n");
PVE::Cluster::cfs_write_file($ceph_cfg_file, $cfg);
print("Successfully updated configuration for 'ceph-crash.service'.\n");
} else {
print("Configuration in '$ceph_cfg_file' does not need to be updated.\n");
}
};
$inner_err = $@;
return 1;
});
# cfs_lock_file sets $@ explicitly to undef
my $err = $@ // '';
my $has_err = !defined($rval) || $inner_err || $err;
if ($has_err) {
$err =~ s/\n*$//;
$inner_err =~ s/\n*$//;
if (!defined($rval)) {
warn("Error while acquiring or releasing lock for '$ceph_cfg_file'.\n");
warn("Error: $err\n") if $err ne '';
}
warn("Failed to configure keyring for 'ceph-crash.service'.\nError: $inner_err\n")
if $inner_err ne '';
exit 1;
}
}
main();

View File

@ -50,7 +50,7 @@ my $opt_verbose;
if (!GetOptions ('verbose' => \$opt_verbose)) {
print_usage ();
exit (-1);
}
}
if (scalar (@ARGV) != 0) {
print_usage ();
@ -76,7 +76,7 @@ __END__
=head1 NAME
pveversion - Proxmox VE version info
pveversion - Proxmox VE version info
=head1 SYNOPSIS

View File

@ -16,3 +16,4 @@
#exclude-path: PATHLIST
#pigz: N
#notes-template: {{guestname}}
#pbs-change-detection-mode: legacy|data|metadata

141
debian/changelog vendored
View File

@ -1,3 +1,144 @@
pve-manager (8.2.4) bookworm; urgency=medium
* pvestatd: clear trailing newlines
* www: advanced backup: add pbs change detection mode selector
* vzdump: add pbs-change-detection-mode to config template
-- Proxmox Support Team <support@proxmox.com> Mon, 10 Jun 2024 13:59:52 +0200
pve-manager (8.2.3) bookworm; urgency=medium
* update shipped appliance info index
* api: add proxmox-firewall to versions pkg list
* gitignore: ignore any test artifacts
* tests: remove vzdump_notification test
* notifications: use named templates instead of in-code templates
-- Proxmox Support Team <support@proxmox.com> Tue, 04 Jun 2024 11:08:41 +0200
pve-manager (8.2.2) bookworm; urgency=medium
* ui: fix form-reset behavior of backup job editor
* ui: backup jobs: fix fleecing parameters for 'run now' button
* ui: mobile: fix login for users that have a TOTP based second factor set up
* ui: mobile: show is setup is lacking the best recommendations for
enterprise production set ups
* vzdump: also warn when hook script fails for backup-abort or log-end phase
* ui: user edit: protect user's TFA settings for new factor variants
* fix #5251: ui: login: set autocomplete on password and user
* ui: esxi importer: try to better convey what live-import does
-- Proxmox Support Team <support@proxmox.com> Tue, 23 Apr 2024 21:33:31 +0200
pve-manager (8.2.1) bookworm; urgency=medium
* d/control: add proxmox-firewall as recommended dependency
* ui: backup job: correctly align descriptions with fields in advanced
options
* ui: backup job: rework empty-text for advanced fields again, mention schema
default values at the end of the descriptions
* ui: acme eab: handle missing meta field in directory response
* ui: qemu: add clipboard selection as advanced option to display editor
* ui: qemu: allow one to enable a vIOMMU (emulated IOMMU) in the machine
editor
* ui: backup job: allow setting up fleecing for a job in advanced config
-- Proxmox Support Team <support@proxmox.com> Mon, 22 Apr 2024 19:36:21 +0200
pve-manager (8.2.0) bookworm; urgency=medium
* node: firewall options: add setting to select new nftables-based firewall
implementation tech preview
* ui: virtual machines: add Windows Server 2025 to OS types
* ui: browser local settings: add new edit-notes-on-double-click option
* fix #4474: ui: guest stop: offer to overrule active shutdown tasks
* ui: backup job: disable 'mailtnotification' field if the notification mode
is set to 'auto'
* ui: backup job: switch order of 'mailto' and 'mailnotification' field
* ui: sdn: QinQ: vlan: properly validate bridge name
* ui: sdn: vlan: fix indentation in vlan edit dialogue
-- Proxmox Support Team <support@proxmox.com> Sun, 21 Apr 2024 13:03:54 +0200
pve-manager (8.1.11) bookworm; urgency=medium
* fix #4136: backup: implement fleecing option for improved guest
stability when the backup target is slow
* api: notifications: add missing 'smtp' to index
* use SSH command helper for pvesh and to get VNC connection info to benefit
from improvements like known host key pinning
* system report: list held-back and pinned APT packages and priorities of APT
sources
* system report: avoid printing the contents of certain wrongly named
configuration files
* system report: output cluster-wide job configuration
* system report: output kernel command line from current boot
* ui: acl: allow searching by typing in the group selector
* api: apt versions: track optional amd64-/intel-microcode and
pve-esxi-import-tools packages
* ui: acme: add External Account Binding (EAB) related fields
* fix #5093: ui: acme: expose custom directory option
* d/postinst: make deb-systemd-invoke non-fatal to avoid potentially
breaking an update for unrelated reasons
* fix #4513: ui: backup job: add tab for advanced options
* vzdump: improve handling performance settings, by using a per-property
fallback and honoring the schema defaults
* ui: lxc: add edit window for device passthrough
* ui: lxc: add firewall log view filtering
* fix #4963: ui: firewall: fix edge case where editing firewall rules using
ips / cidrs would be broken
* fix #1905: ui: VM: allow moving unused disks
* fix #4759: ceph: configure ceph-crash.service and its key
* sdn: evpn: allow empty primary exit node in zone form which was broken by a
change in libpve-network-perl
-- Proxmox Support Team <support@proxmox.com> Fri, 19 Apr 2024 16:24:37 +0200
pve-manager (8.1.10) bookworm; urgency=medium
* guest import: allow setting VLAN-tag for network devices in advanced tab

35
debian/control vendored
View File

@ -11,8 +11,8 @@ Build-Depends: debhelper-compat (= 13),
libpve-access-control (>= 8.0.7),
libpve-cluster-api-perl,
libpve-cluster-perl (>= 6.1-6),
libpve-common-perl (>= 7.2-6),
libpve-guest-common-perl (>= 5.0.2),
libpve-common-perl (>= 8.1.2),
libpve-guest-common-perl (>= 5.1.1),
libpve-http-server-perl (>= 2.0-12),
libpve-notify-perl,
libpve-rs-perl (>= 0.7.1),
@ -60,12 +60,12 @@ Depends: apt (>= 1.5~),
libpve-access-control (>= 8.1.3),
libpve-cluster-api-perl (>= 7.0-5),
libpve-cluster-perl (>= 7.2-3),
libpve-common-perl (>= 7.2-7),
libpve-guest-common-perl (>= 5.0.6),
libpve-common-perl (>= 8.2.0),
libpve-guest-common-perl (>= 5.1.0),
libpve-http-server-perl (>= 4.1-1),
libpve-notify-perl (>= 8.0.5),
libpve-rs-perl (>= 0.7.1),
libpve-storage-perl (>= 8.1.3),
libpve-storage-perl (>= 8.1.5),
librados2-perl (>= 1.3-1),
libtemplate-perl,
libterm-readline-gnu-perl,
@ -74,37 +74,36 @@ Depends: apt (>= 1.5~),
libwww-perl (>= 6.04-1),
logrotate,
lzop,
zstd,
novnc-pve (>= 1.2.0-2~),
pciutils,
perl (>= 5.10.0-19),
postfix | mail-transport-agent,
proxmox-mail-forward,
proxmox-mini-journalreader (>= 1.3-1),
proxmox-widget-toolkit (>= 4.1.5),
proxmox-widget-toolkit (>= 4.2.0),
pve-cluster (>= 8.0.5),
pve-container (>= 5.0.5),
pve-container (>= 5.1.11),
pve-docs (>= 8.0~~),
pve-firewall,
pve-ha-manager,
pve-i18n (>= 3.2.0~),
pve-xtermjs (>= 4.7.0-1),
qemu-server (>= 8.0.4),
qemu-server (>= 8.1.2),
rsync,
spiceterm,
systemd,
vncterm,
wget,
zstd,
${misc:Depends},
${perl:Depends},
${shlibs:Depends}
Recommends: proxmox-offline-mirror-helper, libpve-network-perl (>= 0.9~)
Conflicts: vlan,
vzdump,
Replaces: vlan,
vzdump,
Provides: vlan,
vzdump,
Breaks: libpve-network-perl (<< 0.5-1)
${shlibs:Depends},
Recommends: libpve-network-perl (>= 0.9~),
proxmox-firewall,
proxmox-offline-mirror-helper,
Conflicts: vlan, vzdump,
Replaces: vlan, vzdump,
Provides: vlan, vzdump,
Breaks: libpve-network-perl (<< 0.5-1),
Description: Proxmox Virtual Environment Management Tools
This package contains the Proxmox Virtual Environment management tools.

2
debian/copyright vendored
View File

@ -1,4 +1,4 @@
Copyright (C) 2010-2023 Proxmox Server Solutions GmbH
Copyright (C) 2010-2024 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>

28
debian/postinst vendored
View File

@ -80,6 +80,18 @@ EOF
fi
}
update_ceph_conf() {
UNIT='ceph-crash.service'
# Don't fail in case user has "exotic" configuration where RADOS
# isn't available on all nodes for some reason
/usr/share/pve-manager/helpers/pve-init-ceph-crash || true
if systemctl -q is-enabled "$UNIT" 2> /dev/null; then
deb-systemd-invoke restart "$UNIT" || true
fi
}
migrate_apt_auth_conf() {
output=""
removed=""
@ -123,11 +135,11 @@ case "$1" in
# the ExecStartPre doesn't triggers on service reload, so just in case
pvecm updatecerts --silent || true
deb-systemd-invoke reload-or-try-restart pvedaemon.service
deb-systemd-invoke reload-or-try-restart pvestatd.service
deb-systemd-invoke reload-or-try-restart pveproxy.service
deb-systemd-invoke reload-or-try-restart spiceproxy.service
deb-systemd-invoke reload-or-try-restart pvescheduler.service
deb-systemd-invoke reload-or-try-restart pvedaemon.service || true
deb-systemd-invoke reload-or-try-restart pvestatd.service || true
deb-systemd-invoke reload-or-try-restart pveproxy.service || true
deb-systemd-invoke reload-or-try-restart spiceproxy.service || true
deb-systemd-invoke reload-or-try-restart pvescheduler.service || true
exit 0;;
@ -190,6 +202,10 @@ case "$1" in
set_lvm_conf
if test -n "$2" && dpkg --compare-versions "$2" 'lt' '8.1.11'; then
update_ceph_conf
fi
if test ! -e /proxmox_install_mode; then
# modeled after code generated by dh_start
for unit in ${UNITS}; do
@ -199,7 +215,7 @@ case "$1" in
dh_action="start"
fi
if systemctl -q is-enabled "$unit"; then
deb-systemd-invoke $dh_action "$unit"
deb-systemd-invoke $dh_action "$unit" || true
fi
done
fi

24
templates/Makefile Normal file
View File

@ -0,0 +1,24 @@
NOTIFICATION_TEMPLATES= \
default/test-subject.txt.hbs \
default/test-body.txt.hbs \
default/test-body.html.hbs \
default/vzdump-subject.txt.hbs \
default/vzdump-body.txt.hbs \
default/vzdump-body.html.hbs \
default/replication-subject.txt.hbs \
default/replication-body.txt.hbs \
default/replication-body.html.hbs \
default/package-updates-subject.txt.hbs \
default/package-updates-body.txt.hbs \
default/package-updates-body.html.hbs \
all:
.PHONY: install
install:
install -dm 0755 $(DESTDIR)/usr/share/pve-manager/templates/default
$(foreach i,$(NOTIFICATION_TEMPLATES), \
install -m644 $(i) $(DESTDIR)/usr/share/pve-manager/templates/$(i) ;)
clean:

View File

@ -0,0 +1,6 @@
<html>
<body>
The following updates are available:
{{table updates}}
</body>
</html>

View File

@ -0,0 +1,3 @@
The following updates are available:
{{table updates}}

View File

@ -0,0 +1 @@
New software packages available ({{hostname}})

View File

@ -0,0 +1,18 @@
<html>
<body>
Replication job '{{job-id}}' with target '{{job-target}}' and schedule '{{job-schedule}}' failed!<br/><br/>
Last successful sync: {{timestamp last-sync}}<br/>
Next sync try: {{timestamp next-sync}}<br/>
Failure count: {{failure-count}}<br/>
{{#if (eq failure-count 3)}}
Note: The system will now reduce the frequency of error reports, as the job appears to be stuck.
{{/if}}
<br/>
Error:<br/>
<pre>
{{error}}
</pre>
</body>
</html>

View File

@ -0,0 +1,12 @@
Replication job '{{job-id}}' with target '{{job-target}}' and schedule '{{job-schedule}}' failed!
Last successful sync: {{timestamp last-sync}}
Next sync try: {{timestamp next-sync}}
Failure count: {{failure-count}}
{{#if (eq failure-count 3)}}
Note: The system will now reduce the frequency of error reports, as the job
appears to be stuck.
{{/if}}
Error:
{{ error }}

View File

@ -0,0 +1 @@
Replication Job: '{{job-id}}' failed

View File

@ -0,0 +1 @@
This is a test of the notification target '{{ target }}'.

View File

@ -0,0 +1 @@
This is a test of the notification target '{{ target }}'.

View File

@ -0,0 +1 @@
Test notification

View File

@ -0,0 +1,11 @@
<html>
<body>
{{error-message}}
<h1 style="font-size: 1.2em">Details</h1>
{{table guest-table}}
Total running time: {{duration total-time}}<br/>
Total size: {{human-bytes total-size}}<br/>
<h1 style="font-size: 1.2em">Logs</h1>
<pre>{{logs}}</pre>
</body>
</html>

View File

@ -0,0 +1,10 @@
{{error-message}}
Details
=======
{{table guest-table}}
Total running time: {{duration total-time}}
Total size: {{human-bytes total-size}}
Logs
====
{{logs}}

View File

@ -0,0 +1 @@
vzdump backup status ({{hostname}}): {{status-text}}

View File

@ -5,7 +5,7 @@ all:
export PERLLIB=..
.PHONY: check
check: test-replication test-balloon test-vzdump-notification test-vzdump test-osd
check: test-replication test-balloon test-vzdump test-osd
.PHONY: test-balloon
test-balloon:
@ -17,10 +17,6 @@ test-replication: replication1.t replication2.t replication3.t replication4.t re
replication%.t: replication_test%.pl
./$<
.PHONY: test-vzdump-notification
test-vzdump-notification:
./vzdump_notification_test.pl
.PHONY: test-vzdump
test-vzdump: test-vzdump-guest-included test-vzdump-new

View File

@ -1,101 +0,0 @@
#!/usr/bin/perl
use strict;
use warnings;
use lib '..';
use Test::More tests => 3;
use Test::MockModule;
use PVE::VZDump;
my $STATUS = qr/.*status.*/;
my $NO_LOGFILE = qr/.*Could not open log file.*/;
my $LOG_TOO_LONG = qr/.*Log output was too long.*/;
my $TEST_FILE_PATH = '/tmp/mail_test';
my $TEST_FILE_WRONG_PATH = '/tmp/mail_test_wrong';
sub prepare_mail_with_status {
open(TEST_FILE, '>', $TEST_FILE_PATH); # Removes previous content
print TEST_FILE "start of log file\n";
print TEST_FILE "status: 0\% this should not be in the mail\n";
print TEST_FILE "status: 55\% this should not be in the mail\n";
print TEST_FILE "status: 100\% this should not be in the mail\n";
print TEST_FILE "end of log file\n";
close(TEST_FILE);
}
sub prepare_long_mail {
open(TEST_FILE, '>', $TEST_FILE_PATH); # Removes previous content
# 0.5 MB * 2 parts + the overview tables gives more than 1 MB mail
print TEST_FILE "a" x (1024*1024);
close(TEST_FILE);
}
my $result_text;
my $result_properties;
my $mock_notification_module = Test::MockModule->new('PVE::Notify');
my $mocked_notify = sub {
my ($severity, $title, $text, $properties, $metadata) = @_;
$result_text = $text;
$result_properties = $properties;
};
my $mocked_notify_short = sub {
my (@params) = @_;
return $mocked_notify->('<some severity>', @params);
};
$mock_notification_module->mock(
'notify' => $mocked_notify,
'info' => $mocked_notify_short,
'notice' => $mocked_notify_short,
'warning' => $mocked_notify_short,
'error' => $mocked_notify_short,
);
$mock_notification_module->mock('cfs_read_file', sub {
my $path = shift;
if ($path eq 'datacenter.cfg') {
return {};
} elsif ($path eq 'notifications.cfg' || $path eq 'priv/notifications.cfg') {
return '';
} else {
die "unexpected cfs_read_file\n";
}
});
my $MAILTO = ['test_address@proxmox.com'];
my $SELF = {
opts => { mailto => $MAILTO },
cmdline => 'test_command_on_cli',
};
my $task = { state => 'ok', vmid => '100', };
my $tasklist;
sub prepare_test {
$result_text = undef;
$task->{tmplog} = shift;
$tasklist = [ $task ];
}
{
prepare_test($TEST_FILE_WRONG_PATH);
PVE::VZDump::send_notification($SELF, $tasklist, 0, undef, undef, undef);
like($result_properties->{logs}, $NO_LOGFILE, "Missing logfile is detected");
}
{
prepare_test($TEST_FILE_PATH);
prepare_mail_with_status();
PVE::VZDump::send_notification($SELF, $tasklist, 0, undef, undef, undef);
unlike($result_properties->{"status-text"}, $STATUS, "Status are not in text part of mails");
}
{
prepare_test($TEST_FILE_PATH);
prepare_long_mail();
PVE::VZDump::send_notification($SELF, $tasklist, 0, undef, undef, undef);
like($result_properties->{logs}, $LOG_TOO_LONG, "Text part of mails gets shortened");
}
unlink $TEST_FILE_PATH;

View File

@ -16,6 +16,7 @@ JSSRC= \
data/PermPathStore.js \
data/ResourceStore.js \
data/model/RRDModels.js \
container/TwoColumnContainer.js \
form/ACMEAPISelector.js \
form/ACMEAccountSelector.js \
form/ACMEPluginSelector.js \
@ -97,6 +98,7 @@ JSSRC= \
grid/Replication.js \
grid/ResourceGrid.js \
panel/ConfigPanel.js \
panel/BackupAdvancedOptions.js \
panel/BackupJobPrune.js \
panel/HealthWidget.js \
panel/IPSet.js \
@ -131,6 +133,7 @@ JSSRC= \
window/ScheduleSimulator.js \
window/Wizard.js \
window/GuestDiskReassign.js \
window/GuestStop.js \
window/TreeSettingsEdit.js \
window/PCIMapEdit.js \
window/USBMapEdit.js \
@ -188,6 +191,7 @@ JSSRC= \
lxc/CmdMenu.js \
lxc/Config.js \
lxc/CreateWizard.js \
lxc/DeviceEdit.js \
lxc/DNS.js \
lxc/FeaturesEdit.js \
lxc/MPEdit.js \

View File

@ -51,7 +51,7 @@ Ext.define('PVE.Utils', {
{ desc: '2.4 Kernel', val: 'l24' },
],
'Microsoft Windows': [
{ desc: '11/2022', val: 'win11' },
{ desc: '11/2022/2025', val: 'win11' },
{ desc: '10/2016/2019', val: 'win10' },
{ desc: '8.x/2012/2012r2', val: 'win8' },
{ desc: '7/2008r2', val: 'win7' },
@ -1594,14 +1594,14 @@ Ext.define('PVE.Utils', {
}
},
mp_counts: {
lxc_mp_counts: {
mp: 256,
unused: 256,
},
forEachMP: function(func, includeUnused) {
for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
let cont = func('mp', i);
forEachLxcMP: function(func, includeUnused) {
for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
let cont = func('mp', i, `mp${i}`);
if (!cont && cont !== undefined) {
return;
}
@ -1611,8 +1611,19 @@ Ext.define('PVE.Utils', {
return;
}
for (let i = 0; i < PVE.Utils.mp_counts.unused; i++) {
let cont = func('unused', i);
for (let i = 0; i < PVE.Utils.lxc_mp_counts.unused; i++) {
let cont = func('unused', i, `unused${i}`);
if (!cont && cont !== undefined) {
return;
}
}
},
lxc_dev_count: 256,
forEachLxcDev: function(func) {
for (let i = 0; i < PVE.Utils.lxc_dev_count; i++) {
let cont = func(i, `dev${i}`);
if (!cont && cont !== undefined) {
return;
}
@ -1875,8 +1886,8 @@ Ext.define('PVE.Utils', {
return undefined;
},
nextFreeMP: function(type, config) {
for (let i = 0; i < PVE.Utils.mp_counts[type]; i++) {
nextFreeLxcMP: function(type, config) {
for (let i = 0; i < PVE.Utils.lxc_mp_counts[type]; i++) {
let confid = `${type}${i}`;
if (!Ext.isDefined(config[confid])) {
return {

View File

@ -0,0 +1,57 @@
// This is a container intended to show a field on the first column and one on the second column.
// One can set a ratio for the field sizes.
//
// Works around a limitation of our input panel column1/2 handling that entries are not vertically
// aligned when one of them has wrapping text (like it happens sometimes with such longer
// descriptions)
Ext.define('PVE.container.TwoColumnContainer', {
extend: 'Ext.container.Container',
alias: 'widget.pveTwoColumnContainer',
layout: {
type: 'hbox',
align: 'stretch',
},
// The default ratio of the start widget. It an be an integer or a floating point number
startFlex: 1,
// The default ratio of the end widget. It an be an integer or a floating point number
endFlex: 1,
// the padding between the two columns
columnPadding: 20,
// the config of the first widget
startColumn: undefined,
// the config of the second widget
endColumn: undefined,
// same as fields in a panel
padding: '0 0 5 0',
initComponent: function() {
let me = this;
if (!me.startColumn) {
throw "no start widget configured";
}
if (!me.endColumn) {
throw "no end widget configured";
}
Ext.apply(me, {
items: [
Ext.applyIf({ flex: me.startFlex }, me.startColumn),
{
xtype: 'box',
width: me.columnPadding,
},
Ext.applyIf({ flex: me.endFlex }, me.endColumn),
],
});
me.callParent();
},
});

View File

@ -7,6 +7,7 @@ Ext.define('PVE.dc.BackupEdit', {
defaultFocus: undefined,
subject: gettext("Backup Job"),
width: 720,
bodyPadding: 0,
url: '/api2/extjs/cluster/backup',
@ -145,58 +146,79 @@ Ext.define('PVE.dc.BackupEdit', {
}
},
compressionChange: function(f, value, oldValue) {
this.getView().lookup('backupAdvanced').updateCompression(value, f.isDisabled());
},
compressionDisable: function(f) {
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), true);
},
compressionEnable: function(f) {
this.getView().lookup('backupAdvanced').updateCompression(f.getValue(), false);
},
prepareValues: function(data) {
let me = this;
let viewModel = me.getViewModel();
// Migrate 'new'-old notification-policy back to old-old mailnotification.
// Only should affect users who used pve-manager from pvetest. This was a remnant of
// notifications before the overhaul.
let policy = data['notification-policy'];
if (policy === 'always' || policy === 'failure') {
data.mailnotification = policy;
}
if (data.exclude) {
data.vmid = data.exclude;
data.selMode = 'exclude';
} else if (data.all) {
data.vmid = '';
data.selMode = 'all';
} else if (data.pool) {
data.selMode = 'pool';
data.selPool = data.pool;
} else {
data.selMode = 'include';
}
viewModel.set('selMode', data.selMode);
if (data['prune-backups']) {
Object.assign(data, data['prune-backups']);
delete data['prune-backups'];
} else if (data.maxfiles !== undefined) {
if (data.maxfiles > 0) {
data['keep-last'] = data.maxfiles;
} else {
data['keep-all'] = 1;
}
delete data.maxfiles;
}
if (data['notes-template']) {
data['notes-template'] =
PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
}
if (data.performance) {
Object.assign(data, data.performance);
delete data.performance;
}
return data;
},
init: function(view) {
let me = this;
if (view.isCreate) {
me.lookup('modeSelector').setValue('include');
} else {
view.load({
success: function(response, _options) {
let data = response.result.data;
// Migrate 'new'-old notification-policy back to
// old-old mailnotification. Only should affect
// users who used pve-manager from pvetest.
// This was a remnant of notifications before the
// overhaul.
let policy = data['notification-policy'];
if (policy === 'always' || policy === 'failure') {
data.mailnotification = policy;
}
if (data.exclude) {
data.vmid = data.exclude;
data.selMode = 'exclude';
} else if (data.all) {
data.vmid = '';
data.selMode = 'all';
} else if (data.pool) {
data.selMode = 'pool';
data.selPool = data.pool;
} else {
data.selMode = 'include';
}
me.getViewModel().set('selMode', data.selMode);
if (data['prune-backups']) {
Object.assign(data, data['prune-backups']);
delete data['prune-backups'];
} else if (data.maxfiles !== undefined) {
if (data.maxfiles > 0) {
data['keep-last'] = data.maxfiles;
} else {
data['keep-all'] = 1;
}
delete data.maxfiles;
}
if (data['notes-template']) {
data['notes-template'] =
PVE.Utils.unEscapeNotesTemplate(data['notes-template']);
}
view.setValues(data);
let values = me.prepareValues(response.result.data);
view.setValues(values);
},
});
}
@ -207,13 +229,23 @@ Ext.define('PVE.dc.BackupEdit', {
data: {
selMode: 'include',
notificationMode: '__default__',
mailto: '',
mailNotification: 'always',
},
formulas: {
poolMode: (get) => get('selMode') === 'pool',
disableVMSelection: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
disableVMSelection: (get) => get('selMode') !== 'include' &&
get('selMode') !== 'exclude',
showMailtoFields: (get) =>
['auto', 'legacy-sendmail', '__default__'].includes(get('notificationMode')),
enableMailnotificationField: (get) => {
let mode = get('notificationMode');
let mailto = get('mailto');
return (['auto', '__default__'].includes(mode) && mailto) ||
mode === 'legacy-sendmail';
},
},
},
@ -318,6 +350,7 @@ Ext.define('PVE.dc.BackupEdit', {
],
fieldLabel: gettext('Notification mode'),
name: 'notification-mode',
value: '__default__',
cbind: {
deleteEmpty: '{!isCreate}',
},
@ -325,6 +358,15 @@ Ext.define('PVE.dc.BackupEdit', {
value: '{notificationMode}',
},
},
{
xtype: 'textfield',
fieldLabel: gettext('Send email to'),
name: 'mailto',
bind: {
hidden: '{!showMailtoFields}',
value: '{mailto}',
},
},
{
xtype: 'pveEmailNotificationSelector',
fieldLabel: gettext('Send email'),
@ -334,15 +376,9 @@ Ext.define('PVE.dc.BackupEdit', {
deleteEmpty: '{!isCreate}',
},
bind: {
disabled: '{!showMailtoFields}',
},
},
{
xtype: 'textfield',
fieldLabel: gettext('Send email to'),
name: 'mailto',
bind: {
disabled: '{!showMailtoFields}',
hidden: '{!showMailtoFields}',
disabled: '{!enableMailnotificationField}',
value: '{mailNotification}',
},
},
{
@ -354,6 +390,11 @@ Ext.define('PVE.dc.BackupEdit', {
deleteEmpty: '{!isCreate}',
},
value: 'zstd',
listeners: {
change: 'compressionChange',
disable: 'compressionDisable',
enable: 'compressionEnable',
},
},
{
xtype: 'pveBackupModeSelector',
@ -396,18 +437,6 @@ Ext.define('PVE.dc.BackupEdit', {
},
},
],
advancedColumn1: [
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Repeat missed'),
name: 'repeat-missed',
uncheckedValue: 0,
defaultValue: 0,
cbind: {
deleteDefaultValue: '{!isCreate}',
},
},
],
onGetValues: function(values) {
return this.up('window').getController().onGetValues(values);
},
@ -466,6 +495,14 @@ Ext.define('PVE.dc.BackupEdit', {
},
],
},
{
xtype: 'pveBackupAdvancedOptionsPanel',
reference: 'backupAdvanced',
title: gettext('Advanced'),
cbind: {
isCreate: '{isCreate}',
},
},
],
},
],
@ -515,11 +552,13 @@ Ext.define('PVE.dc.BackupView', {
return;
}
let win = Ext.create('PVE.dc.BackupEdit', {
Ext.create('PVE.dc.BackupEdit', {
autoShow: true,
jobid: rec.data.id,
listeners: {
destroy: () => reload(),
},
});
win.on('destroy', reload);
win.show();
};
let run_detail = function() {
@ -578,7 +617,7 @@ Ext.define('PVE.dc.BackupView', {
delete job['repeat-missed'];
job.all = job.all === true ? 1 : 0;
['performance', 'prune-backups'].forEach(key => {
['performance', 'prune-backups', 'fleecing'].forEach(key => {
if (job[key]) {
job[key] = PVE.Parser.printPropertyString(job[key]);
}

View File

@ -11,7 +11,7 @@ Ext.define('PVE.dc.Tasks', {
let me = this;
let taskstore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'pve-cluster-tasks',
storeId: 'pve-cluster-tasks',
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',

View File

@ -162,7 +162,10 @@ Ext.define('PVE.dc.UserEdit', {
var data = response.result.data;
me.setValues(data);
if (data.keys) {
if (data.keys === 'x!oath' || data.keys === 'x!u2f') {
if (data.keys === 'x' ||
data.keys === 'x!oath' ||
data.keys === 'x!u2f' ||
data.keys === 'x!yubico') {
me.down('[name="keys"]').setDisabled(1);
}
}

View File

@ -62,7 +62,7 @@ Ext.define('PVE.form.SizeField', {
flex: 1,
enableKeyEvents: true,
setValue: function(v) {
if (!this._transformed) {
if (!this._transformed && v !== null) {
let fieldContainer = this.up('fieldcontainer');
let vm = fieldContainer.getViewModel();
let unit = vm.get('unit');

View File

@ -12,6 +12,10 @@ Ext.define('PVE.form.GroupSelector', {
extend: 'Proxmox.form.ComboGrid',
xtype: 'pveGroupSelector',
editable: true,
anyMatch: true,
forceSelection: true,
allowBlank: false,
autoSelect: false,
valueField: 'groupid',

View File

@ -37,8 +37,10 @@ Ext.define('PVE.form.IPRefSelector', {
calculate: function(v) {
if (v.type === 'alias') {
return `${v.scope}/${v.name}`;
} else {
} else if (v.type === 'ipset') {
return `+${v.scope}/${v.name}`;
} else {
return v.ref;
}
},
},
@ -54,15 +56,6 @@ Ext.define('PVE.form.IPRefSelector', {
},
});
var disable_query_for_ips = function(f, value) {
if (value === null ||
value.match(/^\d/)) { // IP address starts with \d
f.queryDelay = 9999999999; // hack: disable with long delay
} else {
f.queryDelay = 10;
}
};
var columns = [];
if (!me.ref_type) {
@ -107,7 +100,9 @@ Ext.define('PVE.form.IPRefSelector', {
},
});
me.on('change', disable_query_for_ips);
me.on('beforequery', function(queryPlan) {
return !(queryPlan.query === null || queryPlan.query.match(/^\d/));
});
me.callParent();
},

View File

@ -83,6 +83,7 @@ Ext.define('PVE.FirewallOptions', {
add_log_row('log_level_out');
add_log_row('tcp_flags_log_level', 120);
add_log_row('smurf_log_level');
add_boolean_row('nftables', gettext('nftables (tech preview)'), 0);
} else if (me.fwtype === 'vm') {
me.rows.enable = {
required: true,

View File

@ -66,7 +66,13 @@ Ext.define('PVE.lxc.CmdMenu', {
iconCls: 'fa fa-fw fa-stop',
disabled: stopped,
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
handler: () => confirmedVMCommand('stop'),
handler: () => {
Ext.create('PVE.GuestStop', {
nodename: info.node,
vm: info,
autoShow: true,
});
},
},
{
text: gettext('Reboot'),

View File

@ -77,11 +77,13 @@ Ext.define('PVE.lxc.Config', {
{
text: gettext('Stop'),
disabled: !caps.vms['VM.PowerMgmt'],
confirmMsg: Proxmox.Utils.format_task_description('vzstop', vmid),
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'CT'),
dangerous: true,
handler: function() {
vm_command("stop");
Ext.create('PVE.GuestStop', {
nodename: nodename,
vm: vm,
autoShow: true,
});
},
iconCls: 'fa fa-stop',
}],
@ -355,6 +357,8 @@ Ext.define('PVE.lxc.Config', {
itemId: 'firewall-fwlog',
xtype: 'proxmoxLogView',
url: '/api2/extjs' + base_url + '/firewall/log',
log_select_timespan: true,
submitFormat: 'U',
},
);
}

View File

@ -0,0 +1,154 @@
Ext.define('PVE.lxc.DeviceInputPanel', {
extend: 'Proxmox.panel.InputPanel',
mixins: ['Proxmox.Mixin.CBind'],
autoComplete: false,
controller: {
xclass: 'Ext.app.ViewController',
},
setVMConfig: function(vmconfig) {
let me = this;
me.vmconfig = vmconfig;
if (me.isCreate) {
PVE.Utils.forEachLxcDev((i, name) => {
if (!Ext.isDefined(vmconfig[name])) {
me.confid = name;
me.down('field[name=devid]').setValue(i);
return false;
}
return undefined;
});
}
},
onGetValues: function(values) {
let me = this;
let confid = me.isCreate ? "dev" + values.devid : me.confid;
delete values.devid;
let val = PVE.Parser.printPropertyString(values, 'path');
let ret = {};
ret[confid] = val;
return ret;
},
items: [
{
xtype: 'proxmoxintegerfield',
name: 'devid',
minValue: 0,
maxValue: PVE.Utils.lxc_dev_count - 1,
hidden: true,
allowBlank: false,
disabled: true,
cbind: {
disabled: '{!isCreate}',
},
},
{
xtype: 'textfield',
name: 'path',
fieldLabel: gettext('Device Path'),
labelWidth: 120,
editable: true,
allowBlank: false,
emptyText: '/dev/xyz',
validator: v => v.startsWith('/dev/') ? true : gettext("Path has to start with /dev/"),
},
],
advancedColumn1: [
{
xtype: 'proxmoxintegerfield',
name: 'uid',
editable: true,
fieldLabel: Ext.String.format(gettext('{0} in CT'), 'UID'),
labelWidth: 120,
emptyText: '0',
minValue: 0,
},
{
xtype: 'proxmoxintegerfield',
name: 'gid',
editable: true,
fieldLabel: Ext.String.format(gettext('{0} in CT'), 'GID'),
labelWidth: 120,
emptyText: '0',
minValue: 0,
},
],
advancedColumn2: [
{
xtype: 'textfield',
name: 'mode',
editable: true,
fieldLabel: Ext.String.format(gettext('Access Mode in CT')),
labelWidth: 120,
emptyText: '0660',
validator: function(value) {
if (/^0[0-7]{3}$|^$/i.test(value)) {
return true;
}
return gettext("Access mode has to be an octal number");
},
},
],
});
Ext.define('PVE.lxc.DeviceEdit', {
extend: 'Proxmox.window.Edit',
vmconfig: undefined,
isAdd: true,
width: 450,
initComponent: function() {
let me = this;
me.isCreate = !me.confid;
let ipanel = Ext.create('PVE.lxc.DeviceInputPanel', {
confid: me.confid,
isCreate: me.isCreate,
pveSelNode: me.pveSelNode,
});
let subject;
if (me.isCreate) {
subject = gettext('Device');
} else {
subject = gettext('Device') + ' (' + me.confid + ')';
}
Ext.apply(me, {
subject: subject,
items: [ipanel],
});
me.callParent();
me.load({
success: function(response, options) {
ipanel.setVMConfig(response.result.data);
if (me.isCreate) {
return;
}
let data = PVE.Parser.parsePropertyString(response.result.data[me.confid], 'path');
let values = {
path: data.path,
mode: data.mode,
uid: data.uid,
gid: data.gid,
};
ipanel.setValues(values);
},
});
},
});

View File

@ -87,8 +87,7 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
let me = this;
me.updateVMConfig(vmconfig);
PVE.Utils.forEachMP((bus, i) => {
let name = "mp" + i.toString();
PVE.Utils.forEachLxcMP((bus, i, name) => {
if (!Ext.isDefined(vmconfig[name])) {
me.down('field[name=mpid]').setValue(i);
return false;
@ -194,7 +193,7 @@ Ext.define('PVE.lxc.MountPointInputPanel', {
name: 'mpid',
fieldLabel: gettext('Mount Point ID'),
minValue: 0,
maxValue: PVE.Utils.mp_counts.mp - 1,
maxValue: PVE.Utils.lxc_mp_counts.mp - 1,
hidden: true,
allowBlank: false,
disabled: true,

View File

@ -8,7 +8,7 @@ Ext.define('PVE.lxc.MultiMPPanel', {
xclass: 'Ext.app.ViewController',
// count of mps + rootfs
maxCount: PVE.Utils.mp_counts.mp + 1,
maxCount: PVE.Utils.lxc_mp_counts.mp + 1,
getNextFreeDisk: function(vmconfig) {
let nextFreeDisk;
@ -17,7 +17,7 @@ Ext.define('PVE.lxc.MultiMPPanel', {
confid: 'rootfs',
};
} else {
for (let i = 0; i < PVE.Utils.mp_counts.mp; i++) {
for (let i = 0; i < PVE.Utils.lxc_mp_counts.mp; i++) {
let confid = `mp${i}`;
if (!vmconfig[confid]) {
nextFreeDisk = {

View File

@ -28,7 +28,6 @@ Ext.define('PVE.lxc.RessourceView', {
initComponent: function() {
var me = this;
let confid;
var nodename = me.pveSelNode.data.node;
if (!nodename) {
@ -116,8 +115,7 @@ Ext.define('PVE.lxc.RessourceView', {
},
};
PVE.Utils.forEachMP(function(bus, i) {
confid = bus + i;
PVE.Utils.forEachLxcMP(function(bus, i, confid) {
var group = 5;
var header;
if (bus === 'mp') {
@ -135,6 +133,18 @@ Ext.define('PVE.lxc.RessourceView', {
};
}, true);
let deveditor = Proxmox.UserName === 'root@pam' ? 'PVE.lxc.DeviceEdit' : undefined;
PVE.Utils.forEachLxcDev(function(i, confid) {
rows[confid] = {
group: 7,
order: i,
tdCls: 'pve-itype-icon-pci',
editor: deveditor,
header: gettext('Device') + ' (' + confid + ')',
};
});
var baseurl = 'nodes/' + nodename + '/lxc/' + vmid + '/config';
me.selModel = Ext.create('Ext.selection.RowModel', {});
@ -311,6 +321,7 @@ Ext.define('PVE.lxc.RessourceView', {
let isDisk = isRootFS || key.match(/^(mp|unused)\d+/);
let isUnusedDisk = key.match(/^unused\d+/);
let isUsedDisk = isDisk && !isUnusedDisk;
let isDevice = key.match(/^dev\d+/);
let noedit = isDelete || !rowdef.editor;
if (!noedit && Proxmox.UserName !== 'root@pam' && key.match(/^mp\d+$/)) {
@ -326,7 +337,7 @@ Ext.define('PVE.lxc.RessourceView', {
reassign_menuitem.setDisabled(isRootFS);
resize_menuitem.setDisabled(isUnusedDisk);
remove_btn.setDisabled(!isDisk || isRootFS || !diskCap || pending);
remove_btn.setDisabled(!(isDisk || isDevice) || isRootFS || !diskCap || pending);
revert_btn.setDisabled(!pending);
remove_btn.setText(isUsedDisk ? remove_btn.altText : remove_btn.defaultText);
@ -380,6 +391,21 @@ Ext.define('PVE.lxc.RessourceView', {
});
},
},
{
text: gettext('Device Passthrough'),
iconCls: 'pve-itype-icon-pci',
disabled: Proxmox.UserName !== 'root@pam',
handler: function() {
Ext.create('PVE.lxc.DeviceEdit', {
autoShow: true,
url: `/api2/extjs/${baseurl}`,
pveSelNode: me.pveSelNode,
listeners: {
destroy: () => me.reload(),
},
});
},
},
],
}),
},

View File

@ -10,6 +10,20 @@ Ext.define('PVE.node.ACMEAccountCreate', {
url: '/cluster/acme/account',
showTaskViewer: true,
defaultExists: false,
referenceHolder: true,
onlineHelp: "sysadmin_certs_acme_account",
viewModel: {
data: {
customDirectory: false,
eabRequired: false,
},
formulas: {
eabEmptyText: function(get) {
return get('eabRequired') ? gettext("required") : gettext("optional");
},
},
},
items: [
{
@ -30,12 +44,18 @@ Ext.define('PVE.node.ACMEAccountCreate', {
},
{
xtype: 'proxmoxComboGrid',
name: 'directory',
notFoundIsValid: true,
isFormField: false,
allowBlank: false,
valueField: 'url',
displayField: 'name',
fieldLabel: gettext('ACME Directory'),
store: {
listeners: {
'load': function() {
this.add({ name: gettext("Custom"), url: '' });
},
},
autoLoad: true,
fields: ['name', 'url'],
idProperty: ['name'],
@ -43,10 +63,6 @@ Ext.define('PVE.node.ACMEAccountCreate', {
type: 'proxmox',
url: '/api2/json/cluster/acme/directories',
},
sorters: {
property: 'name',
direction: 'ASC',
},
},
listConfig: {
columns: [
@ -64,42 +80,99 @@ Ext.define('PVE.node.ACMEAccountCreate', {
},
listeners: {
change: function(combogrid, value) {
var me = this;
if (!value) {
return;
let me = this;
let vm = me.up('window').getViewModel();
let dirField = me.up('window').lookupReference('directoryInput');
let tosButton = me.up('window').lookupReference('queryTos');
let isCustom = combogrid.getSelection().get('name') === gettext("Custom");
vm.set('customDirectory', isCustom);
dirField.setValue(value);
if (!isCustom) {
tosButton.click();
} else {
me.up('window').clearToSFields();
}
var disp = me.up('window').down('#tos_url_display');
var field = me.up('window').down('#tos_url');
var checkbox = me.up('window').down('#tos_checkbox');
disp.setValue(gettext('Loading'));
field.setValue(undefined);
checkbox.setValue(undefined);
checkbox.setHidden(true);
Proxmox.Utils.API2Request({
url: '/cluster/acme/meta',
method: 'GET',
params: {
directory: value,
},
success: function(response, opt) {
if (response.result.data.termsOfService) {
field.setValue(response.result.data.termsOfService);
disp.setValue(response.result.data.termsOfService);
checkbox.setHidden(false);
} else {
disp.setValue(undefined);
}
},
failure: function(response, opt) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
},
},
{
xtype: 'fieldcontainer',
layout: 'hbox',
fieldLabel: gettext('URL'),
bind: {
hidden: '{!customDirectory}',
},
items: [
{
xtype: 'proxmoxtextfield',
name: 'directory',
reference: 'directoryInput',
flex: 1,
allowBlank: false,
listeners: {
change: function(textbox, value) {
let me = this;
me.up('window').clearToSFields();
},
},
},
{
xtype: 'proxmoxButton',
margin: '0 0 0 5',
reference: 'queryTos',
text: gettext('Query URL'),
listeners: {
click: function(button) {
let me = this;
let w = me.up('window');
let vm = w.getViewModel();
let disp = w.down('#tos_url_display');
let field = w.down('#tos_url');
let checkbox = w.down('#tos_checkbox');
let value = w.lookupReference('directoryInput').getValue();
w.clearToSFields();
if (!value) {
return;
} else {
disp.setValue(gettext("Loading"));
}
Proxmox.Utils.API2Request({
url: '/cluster/acme/meta',
method: 'GET',
params: {
directory: value,
},
success: function(response, opt) {
if (response.result.data && response.result.data.termsOfService) {
field.setValue(response.result.data.termsOfService);
disp.setValue(response.result.data.termsOfService);
checkbox.setHidden(false);
} else {
// Needed to pass input verification and enable register button
// has no influence on the submitted form
checkbox.setValue(true);
disp.setValue("No terms of service agreement required");
}
vm.set('eabRequired', !!(response.result.data &&
response.result.data.externalAccountRequired));
},
failure: function(response, opt) {
disp.setValue(undefined);
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
},
},
],
},
{
xtype: 'displayfield',
itemId: 'tos_url_display',
@ -123,8 +196,41 @@ Ext.define('PVE.node.ACMEAccountCreate', {
return false;
},
},
{
xtype: 'proxmoxtextfield',
name: 'eab-kid',
fieldLabel: gettext('EAB Key ID'),
bind: {
hidden: '{!customDirectory}',
allowBlank: '{!eabRequired}',
emptyText: '{eabEmptyText}',
},
},
{
xtype: 'proxmoxtextfield',
name: 'eab-hmac-key',
fieldLabel: gettext('EAB Key'),
bind: {
hidden: '{!customDirectory}',
allowBlank: '{!eabRequired}',
emptyText: '{eabEmptyText}',
},
},
],
clearToSFields: function() {
let me = this;
let disp = me.down('#tos_url_display');
let field = me.down('#tos_url');
let checkbox = me.down('#tos_checkbox');
disp.setValue("Terms of service not fetched yet");
field.setValue(undefined);
checkbox.setValue(undefined);
checkbox.setHidden(true);
},
});
Ext.define('PVE.node.ACMEAccountView', {

View File

@ -0,0 +1,269 @@
/*
* Input panel for advanced backup options intended to be used as part of an edit/create window.
*/
Ext.define('PVE.panel.BackupAdvancedOptions', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pveBackupAdvancedOptionsPanel',
mixins: ['Proxmox.Mixin.CBind'],
cbindData: function() {
let me = this;
me.isCreate = !!me.isCreate;
return {};
},
viewModel: {
data: {},
},
controller: {
xclass: 'Ext.app.ViewController',
toggleFleecing: function(cb, value) {
let me = this;
me.lookup('fleecingStorage').setDisabled(!value);
},
control: {
'proxmoxcheckbox[reference=fleecingEnabled]': {
change: 'toggleFleecing',
},
},
},
onGetValues: function(formValues) {
let me = this;
if (me.needMask) { // isMasked() may not yet be true if not rendered once
return {};
}
let options = {};
if (!me.isCreate) {
options.delete = []; // to avoid having to check this all the time
}
const deletePropertyOnEdit = me.isCreate
? () => { /* no-op on create */ }
: key => options.delete.push(key);
let fleecing = {}, fleecingOptions = ['fleecing-enabled', 'fleecing-storage'];
let performance = {}, performanceOptions = ['max-workers', 'pbs-entries-max'];
for (const [key, value] of Object.entries(formValues)) {
if (performanceOptions.includes(key)) {
performance[key] = value;
// deleteEmpty is not currently implemented for pveBandwidthField
} else if (key === 'bwlimit' && value === '') {
deletePropertyOnEdit('bwlimit');
} else if (key === 'delete') {
if (Array.isArray(value)) {
value.filter(opt => !performanceOptions.includes(opt)).forEach(
opt => deletePropertyOnEdit(opt),
);
} else if (!performanceOptions.includes(formValues.delete)) {
deletePropertyOnEdit(value);
}
} else if (fleecingOptions.includes(key)) {
let fleecingKey = key.slice('fleecing-'.length);
fleecing[fleecingKey] = value;
} else {
options[key] = value;
}
}
if (Object.keys(performance).length > 0) {
options.performance = PVE.Parser.printPropertyString(performance);
} else {
deletePropertyOnEdit('performance');
}
if (Object.keys(fleecing).length > 0) {
options.fleecing = PVE.Parser.printPropertyString(fleecing);
} else {
deletePropertyOnEdit('fleecing');
}
if (me.isCreate) {
delete options.delete;
}
return options;
},
onSetValues: function(values) {
if (values.fleecing) {
for (const [key, value] of Object.entries(values.fleecing)) {
values[`fleecing-${key}`] = value;
}
delete values.fleecing;
}
if (values["pbs-change-detection-mode"] === '__default__') {
delete values["pbs-change-detection-mode"];
}
return values;
},
updateCompression: function(value, disabled) {
this.lookup('zstdThreadCount').setDisabled(disabled || value !== 'zstd');
},
items: [
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'pveBandwidthField',
name: 'bwlimit',
fieldLabel: gettext('Bandwidth Limit'),
emptyText: gettext('Fallback'),
backendUnit: 'KiB',
allowZero: true,
emptyValue: '',
autoEl: {
tag: 'div',
'data-qtip': Ext.String.format(gettext('Use {0} for unlimited'), 0),
},
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: `${gettext('Limit I/O bandwidth.')} ${Ext.String.format(gettext("Schema default: {0}"), 0)}`,
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'proxmoxintegerfield',
name: 'zstd',
reference: 'zstdThreadCount',
fieldLabel: Ext.String.format(gettext('{0} Threads'), 'Zstd'),
fieldStyle: 'text-align: right',
emptyText: gettext('Fallback'),
minValue: 0,
cbind: {
deleteEmpty: '{!isCreate}',
},
autoEl: {
tag: 'div',
'data-qtip': gettext('With 0, half of the available cores are used'),
},
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: `${gettext('Threads used for zstd compression (non-PBS).')} ${Ext.String.format(gettext("Schema default: {0}"), 1)}`,
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'proxmoxintegerfield',
name: 'max-workers',
minValue: 1,
maxValue: 256,
fieldLabel: gettext('IO-Workers'),
fieldStyle: 'text-align: right',
emptyText: gettext('Fallback'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: `${gettext('I/O workers in the QEMU process (VMs only).')} ${Ext.String.format(gettext("Schema default: {0}"), 16)}`,
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'proxmoxcheckbox',
name: 'fleecing-enabled',
reference: 'fleecingEnabled',
fieldLabel: gettext('Fleecing'),
uncheckedValue: 0,
value: 0,
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: gettext('Backup write cache that can reduce IO pressure inside guests (VMs only).'),
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'pveStorageSelector',
name: 'fleecing-storage',
fieldLabel: gettext('Fleecing Storage'),
reference: 'fleecingStorage',
clusterView: true,
storageContent: 'images',
allowBlank: false,
disabled: true,
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: gettext('Prefer a fast and local storage, ideally with support for discard and thin-provisioning or sparse files.'),
},
},
{
// It's part of the 'performance' property string, so have a field to preserve the
// value, but don't expose it. It's a rather niche setting and difficult to
// convey/understand what it does.
xtype: 'proxmoxintegerfield',
name: 'pbs-entries-max',
hidden: true,
fieldLabel: 'TODO',
fieldStyle: 'text-align: right',
emptyText: 'TODO',
cbind: {
deleteEmpty: '{!isCreate}',
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Repeat missed'),
name: 'repeat-missed',
uncheckedValue: 0,
defaultValue: 0,
cbind: {
deleteDefaultValue: '{!isCreate}',
},
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: gettext("Run jobs as soon as possible if they couldn't start on schedule, for example, due to the node being offline."),
},
},
{
xtype: 'pveTwoColumnContainer',
startColumn: {
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('PBS change detection mode'),
name: 'pbs-change-detection-mode',
deleteEmpty: true,
value: '__default__',
comboItems: [
['__default__', "Default"],
['data', "Data"],
['metadata', "Metadata"],
],
},
endFlex: 2,
endColumn: {
xtype: 'displayfield',
value: gettext("EXPERIMENTAL: Mode to detect file changes and archive encoding format for container backups."),
},
},
{
xtype: 'component',
padding: '5 1',
html: `<span class="pmx-hint">${gettext('Note')}</span>: ${
gettext("The node-specific 'vzdump.conf' or, if this is not set, the default from the config schema is used to determine fallback values.")}`,
},
],
});

View File

@ -93,7 +93,13 @@ Ext.define('PVE.qemu.CmdMenu', {
iconCls: 'fa fa-fw fa-stop',
disabled: stopped,
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
handler: () => confirmedVMCommand('stop'),
handler: () => {
Ext.create('PVE.GuestStop', {
nodename: info.node,
vm: info,
autoShow: true,
});
},
},
{
text: gettext('Reboot'),

View File

@ -176,11 +176,13 @@ Ext.define('PVE.qemu.Config', {
}, {
text: gettext('Stop'),
disabled: !caps.vms['VM.PowerMgmt'],
dangerous: true,
tooltip: Ext.String.format(gettext('Stop {0} immediately'), 'VM'),
confirmMsg: Proxmox.Utils.format_task_description('qmstop', vmid),
handler: function() {
vm_command("stop", { timeout: 30 });
Ext.create('PVE.GuestStop', {
nodename: nodename,
vm: vm,
autoShow: true,
});
},
iconCls: 'fa fa-stop',
}, {

View File

@ -11,6 +11,36 @@ Ext.define('PVE.qemu.DisplayInputPanel', {
return { vga: ret };
},
viewModel: {
data: {
type: '__default__',
clipboard: '__default__',
},
formulas: {
matchNonGUIOption: function(get) {
return get('type').match(/^(serial\d|none)$/);
},
memoryEmptyText: function(get) {
let val = get('type');
if (val === "cirrus") {
return "4";
} else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") {
return "16";
} else if (val.match(/^virtio/)) {
return "256";
} else if (get('matchNonGUIOption')) {
return "N/A";
} else {
console.debug("unexpected display type", val);
return Proxmox.Utils.defaultText;
}
},
isVNC: get => get('clipboard') === 'vnc',
hideDefaultHint: get => get('isVNC') || get('matchNonGUIOption'),
hideVNCHint: get => !get('isVNC') || get('matchNonGUIOption'),
},
},
items: [{
name: 'type',
xtype: 'proxmoxKVComboBox',
@ -27,29 +57,8 @@ Ext.define('PVE.qemu.DisplayInputPanel', {
}
return true;
},
listeners: {
change: function(cb, val) {
if (!val) {
return;
}
let memoryfield = this.up('panel').down('field[name=memory]');
let disableMemoryField = false;
if (val === "cirrus") {
memoryfield.setEmptyText("4");
} else if (val === "std" || val.match(/^qxl\d?$/) || val === "vmware") {
memoryfield.setEmptyText("16");
} else if (val.match(/^virtio/)) {
memoryfield.setEmptyText("256");
} else if (val.match(/^(serial\d|none)$/)) {
memoryfield.setEmptyText("N/A");
disableMemoryField = true;
} else {
console.debug("unexpected display type", val);
memoryfield.setEmptyText(Proxmox.Utils.defaultText);
}
memoryfield.setDisabled(disableMemoryField);
},
bind: {
value: '{type}',
},
},
{
@ -60,7 +69,49 @@ Ext.define('PVE.qemu.DisplayInputPanel', {
maxValue: 512,
step: 4,
name: 'memory',
bind: {
emptyText: '{memoryEmptyText}',
disabled: '{matchNonGUIOption}',
},
}],
advancedItems: [
{
xtype: 'proxmoxKVComboBox',
name: 'clipboard',
deleteEmpty: false,
value: '__default__',
fieldLabel: gettext('Clipboard'),
comboItems: [
['__default__', Proxmox.Utils.defaultText],
['vnc', 'VNC'],
],
bind: {
value: '{clipboard}',
disabled: '{matchNonGUIOption}',
},
},
{
xtype: 'displayfield',
name: 'vncHint',
userCls: 'pmx-hint',
value: gettext('You cannot use the default SPICE clipboard if the VNC Clipboard is selected.') + ' ' +
gettext('VNC Clipboard requires spice-tools installed in the Guest-VM.'),
bind: {
hidden: '{hideVNCHint}',
},
},
{
xtype: 'displayfield',
name: 'defaultHint',
userCls: 'pmx-hint',
value: gettext('This option depends on your display type.') + ' ' +
gettext('If the display type uses SPICE you are able to use the default SPICE Clipboard.'),
bind: {
hidden: '{hideDefaultHint}',
},
},
],
});
Ext.define('PVE.qemu.DisplayEdit', {

View File

@ -634,7 +634,6 @@ Ext.define('PVE.qemu.HardwareView', {
isCloudInit ||
!(isDisk || isEfi || tpmMoveable),
);
move_menuitem.setDisabled(isUnusedDisk);
reassign_menuitem.setDisabled(pending || (isEfi || tpmMoveable));
resize_menuitem.setDisabled(pending || !isUsedDisk);

View File

@ -1,6 +1,16 @@
Ext.define('PVE.qemu.MachineInputPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pveMachineInputPanel',
onlineHelp: 'qm_machine_type',
viewModel: {
data: {
type: '__default__',
},
formulas: {
q35: get => get('type') === 'q35',
},
},
controller: {
xclass: 'Ext.app.ViewController',
@ -35,17 +45,29 @@ Ext.define('PVE.qemu.MachineInputPanel', {
},
onGetValues: function(values) {
if (values.delete === 'machine' && values.viommu) {
delete values.delete;
values.machine = 'pc';
}
if (values.version && values.version !== 'latest') {
values.machine = values.version;
delete values.delete;
}
delete values.version;
return values;
if (values.delete === 'machine' && !values.viommu) {
return values;
}
let ret = {};
ret.machine = PVE.Parser.printPropertyString(values, 'machine');
return ret;
},
setValues: function(values) {
let me = this;
let machineConf = PVE.Parser.parsePropertyString(values.machine, 'type');
values.machine = machineConf.type;
me.isWindows = values.isWindows;
if (values.machine === 'pc') {
values.machine = '__default__';
@ -58,6 +80,9 @@ Ext.define('PVE.qemu.MachineInputPanel', {
values.version = 'pc-q35-5.1';
}
}
values.viommu = machineConf.viommu || '__default__';
if (values.machine !== '__default__' && values.machine !== 'q35') {
values.version = values.machine;
values.machine = values.version.match(/q35/) ? 'q35' : '__default__';
@ -78,6 +103,9 @@ Ext.define('PVE.qemu.MachineInputPanel', {
['__default__', PVE.Utils.render_qemu_machine('')],
['q35', 'q35'],
],
bind: {
value: '{type}',
},
},
advancedItems: [
@ -113,6 +141,39 @@ Ext.define('PVE.qemu.MachineInputPanel', {
fieldLabel: gettext('Note'),
value: gettext('Machine version change may affect hardware layout and settings in the guest OS.'),
},
{
xtype: 'proxmoxKVComboBox',
name: 'viommu',
fieldLabel: gettext('vIOMMU'),
reference: 'viommu-q35',
deleteEmpty: false,
value: '__default__',
comboItems: [
['__default__', Proxmox.Utils.defaultText + ' (None)'],
['intel', gettext('Intel (AMD Compatible)')],
['virtio', 'VirtIO'],
],
bind: {
hidden: '{!q35}',
disabled: '{!q35}',
},
},
{
xtype: 'proxmoxKVComboBox',
name: 'viommu',
fieldLabel: gettext('vIOMMU'),
reference: 'viommu-i440fx',
deleteEmpty: false,
value: '__default__',
comboItems: [
['__default__', Proxmox.Utils.defaultText + ' (None)'],
['virtio', 'VirtIO'],
],
bind: {
hidden: '{q35}',
disabled: '{q35}',
},
},
],
});

View File

@ -54,6 +54,8 @@ Ext.define('PVE.sdn.zones.EvpnInputPanel', {
fieldLabel: gettext('Primary Exit Node'),
multiSelect: false,
autoSelect: false,
skipEmptyText: true,
deleteEmpty: !me.isCreate,
},
{
xtype: 'proxmoxcheckbox',

View File

@ -24,6 +24,9 @@ Ext.define('PVE.sdn.zones.QinQInputPanel', {
name: 'bridge',
fieldLabel: 'Bridge',
allowBlank: false,
vtype: 'BridgeName',
minLength: 1,
maxLength: 10,
},
{
xtype: 'proxmoxintegerfield',

View File

@ -20,10 +20,13 @@ Ext.define('PVE.sdn.zones.VlanInputPanel', {
me.items = [
{
xtype: 'textfield',
name: 'bridge',
fieldLabel: 'Bridge',
allowBlank: false,
xtype: 'textfield',
name: 'bridge',
fieldLabel: 'Bridge',
allowBlank: false,
vtype: 'BridgeName',
minLength: 1,
maxLength: 10,
},
];

View File

@ -17,8 +17,8 @@ Ext.define('PVE.window.GuestDiskReassign', {
},
formulas: {
mpMaxCount: get => get('mpType') === 'mp'
? PVE.Utils.mp_counts.mps - 1
: PVE.Utils.mp_counts.unused - 1,
? PVE.Utils.lxc_mp_counts.mps - 1
: PVE.Utils.lxc_mp_counts.unused - 1,
},
},
@ -103,7 +103,7 @@ Ext.define('PVE.window.GuestDiskReassign', {
view.VMConfig = result.data;
mpIdSelector.setValue(
PVE.Utils.nextFreeMP(
PVE.Utils.nextFreeLxcMP(
view.getViewModel().get('mpType'),
view.VMConfig,
).id,

View File

@ -561,10 +561,17 @@ Ext.define('PVE.window.GuestImport', {
fieldLabel: gettext('Live Import'),
reference: 'liveimport',
isFormField: false,
boxLabelCls: 'pmx-hint black x-form-cb-label',
boxLabel: gettext('Starts a previously stopped VM on Proxmox VE and imports the disks in the background.'),
bind: {
value: '{liveImport}',
boxLabel: '{liveImportNote}',
},
},
{
xtype: 'displayfield',
userCls: 'pmx-hint black',
value: gettext('Note: If anything goes wrong during the live-import, new data written by the VM may be lost.'),
bind: {
hidden: '{!liveImport}',
},
},
{

View File

@ -0,0 +1,77 @@
Ext.define('PVE.GuestStop', {
extend: 'Ext.window.MessageBox',
closeAction: 'destroy',
initComponent: function() {
let me = this;
if (!me.nodename) {
throw "no node name specified";
}
if (!me.vm) {
throw "no vm specified";
}
let isQemuVM = me.vm.type === 'qemu';
let overruleTaskType = isQemuVM ? 'qmshutdown' : 'vzshutdown';
me.taskType = isQemuVM ? 'qmstop' : 'vzstop';
me.url = `/nodes/${me.nodename}/${me.vm.type}/${me.vm.vmid}/status/stop`;
let caps = Ext.state.Manager.get('GuiCap');
let hasSysModify = !!caps.nodes['Sys.Modify'];
// offer to overrule if there is at least one matching shutdown task and the guest is not
// HA-enabled. Also allow users to abort tasks started by one of their API tokens.
let activeShutdownTask = Ext.getStore('pve-cluster-tasks')?.findBy(task =>
(hasSysModify || task.data.user === Proxmox.UserName) &&
task.data.id === me.vm.vmid.toString() &&
task.data.status === undefined &&
task.data.type === overruleTaskType,
) !== -1;
let haEnabled = me.vm.hastate && me.vm.hastate !== 'unmanaged';
me.callParent();
// message box has its actual content in a sub-container, the top one is just for layouting
me.promptContainer.add({
xtype: 'proxmoxcheckbox',
name: 'overrule-shutdown',
checked: !haEnabled && activeShutdownTask,
boxLabel: gettext('Overrule active shutdown tasks'),
hidden: !(hasSysModify || activeShutdownTask),
disabled: !(hasSysModify || activeShutdownTask) || haEnabled,
padding: '3 0 0 0',
});
},
handler: function(btn) {
let me = this;
if (btn === 'yes') {
let overruleField = me.promptContainer.down('proxmoxcheckbox[name=overrule-shutdown]');
let params = !overruleField.isDisabled() && overruleField.getSubmitValue()
? { 'overrule-shutdown': 1 }
: undefined;
Proxmox.Utils.API2Request({
url: me.url,
waitMsgTarget: me,
method: 'POST',
params: params,
failure: response => Ext.Msg.alert('Error', response.htmlStatus),
});
}
},
show: function() {
let me = this;
let cfg = {
title: gettext('Confirm'),
icon: Ext.Msg.WARNING,
msg: Proxmox.Utils.format_task_description(me.taskType, me.vm.vmid),
buttons: Ext.Msg.YESNO,
callback: btn => me.handler(btn),
};
me.callParent([cfg]);
},
});

View File

@ -344,6 +344,7 @@ Ext.define('PVE.window.LoginWindow', {
itemId: 'usernameField',
reference: 'usernameField',
stateId: 'login-username',
inputAttrTpl: 'autocomplete=username',
bind: {
visible: "{!openid}",
disabled: "{openid}",
@ -355,6 +356,7 @@ Ext.define('PVE.window.LoginWindow', {
fieldLabel: gettext('Password'),
name: 'password',
reference: 'passwordField',
inputAttrTpl: 'autocomplete=current-password',
bind: {
visible: "{!openid}",
disabled: "{openid}",

View File

@ -41,6 +41,7 @@ Ext.define('PVE.window.Settings', {
me.lookup('summarycolumns').setValue(summarycolumns);
me.lookup('guestNotesCollapse').setValue(sp.get('guest-notes-collapse', 'never'));
me.lookup('editNotesOnDoubleClick').setValue(sp.get('edit-notes-on-double-click', false));
var settings = ['fontSize', 'fontFamily', 'letterSpacing', 'lineHeight'];
settings.forEach(function(setting) {
@ -146,6 +147,9 @@ Ext.define('PVE.window.Settings', {
'field[reference=guestNotesCollapse]': {
change: (e, v) => Ext.state.Manager.getProvider().set('guest-notes-collapse', v),
},
'field[reference=editNotesOnDoubleClick]': {
change: (e, v) => Ext.state.Manager.getProvider().set('edit-notes-on-double-click', v),
},
},
},
@ -250,7 +254,7 @@ Ext.define('PVE.window.Settings', {
{
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('Summary columns') + ':',
labelWidth: 150,
labelWidth: 125,
stateId: 'summarycolumns',
reference: 'summarycolumns',
comboItems: [
@ -263,7 +267,7 @@ Ext.define('PVE.window.Settings', {
{
xtype: 'proxmoxKVComboBox',
fieldLabel: gettext('Guest Notes') + ':',
labelWidth: 150,
labelWidth: 125,
stateId: 'guest-notes-collapse',
reference: 'guestNotesCollapse',
comboItems: [
@ -272,6 +276,15 @@ Ext.define('PVE.window.Settings', {
['auto', 'auto (Collapse if empty)'],
],
},
{
xtype: 'checkbox',
fieldLabel: gettext('Notes'),
labelWidth: 125,
boxLabel: gettext('Open editor on double-click'),
reference: 'editNotesOnDoubleClick',
inputValue: true,
uncheckedValue: false,
},
],
},
{

View File

@ -35,8 +35,12 @@ Ext.define('PVE.Login', {
message: 'Loading...',
});
Proxmox.Utils.API2Request({
url: '/api2/extjs/access/tfa',
params: { response: code },
url: '/api2/extjs/access/ticket',
params: {
username: ticketResponse.username,
'tfa-challenge': ticketResponse.ticket,
password: `totp:${code}`
},
method: 'POST',
timeout: 5000, // it'll delay both success & failure
success: function(resp, opts) {

View File

@ -7,9 +7,4 @@ Ext.Ajax.setDisableCaching(false);
// do not send '_dc' parameter
Ext.Ajax.disableCaching = false;
Ext.MessageBox = Ext.Msg = {
alert: (title, message) => console.warn(title, message),
show: ({ title, message }) => console.warn(title, message),
};
Ext.Loader.injectScriptElement = (url) => console.warn(`surpressed loading ${url}`);

View File

@ -111,6 +111,8 @@ Ext.define('PVE.Workspace', {
// also sets the cookie
Proxmox.Utils.setAuthData(loginData);
Proxmox.Utils.checked_command(Ext.emptyFn); // display subscription status
PVE.Workspace.gotoPage('');
},