auto-format code using perltidy with Proxmox style guide

using the new top-level `make tidy` target, which calls perltidy via
our wrapper to enforce the desired style as closely as possible.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2025-06-17 18:22:29 +02:00
parent a750c44c42
commit 138cae897a
88 changed files with 19180 additions and 17920 deletions

View file

@ -18,119 +18,121 @@ use PVE::API2::Pool;
use PVE::API2::AccessControl;
use PVE::API2::Storage::Config;
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Cluster",
path => 'cluster',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Nodes",
path => 'nodes',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Storage::Config",
path => 'storage',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::AccessControl",
path => 'access',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Pool",
path => 'pools',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Directory index.",
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [{ rel => 'child', href => "{subdir}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [ { subdir => 'version' } ];
my $res = [{ subdir => 'version' }];
my $ma = PVE::API2->method_attributes();
my $ma = PVE::API2->method_attributes();
foreach my $info (@$ma) {
next if !$info->{subclass};
foreach my $info (@$ma) {
next if !$info->{subclass};
my $subpath = $info->{match_re}->[0];
my $subpath = $info->{match_re}->[0];
push @$res, { subdir => $subpath };
}
push @$res, { subdir => $subpath };
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'version',
path => 'version',
method => 'GET',
permissions => { user => 'all' },
description => "API version details, including some parts of the global datacenter config.",
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => "object",
properties => {
version => {
type => 'string',
description => 'The full pve-manager package version of this node.',
},
release => {
type => 'string',
description => 'The current Proxmox VE point release in `x.y` format.',
},
repoid => {
type => 'string',
# length 8 is old (< 8.0) short-id, 16 is new short id, 40 is sha1 and 64 is sha256
pattern => '[0-9a-fA-F]{8,64}',
description => 'The short git revision from which this version was build.',
},
console => {
type => 'string',
enum => ['applet', 'vv', 'html5', 'xtermjs'],
optional => 1,
description => 'The default console viewer to use.',
},
},
type => "object",
properties => {
version => {
type => 'string',
description => 'The full pve-manager package version of this node.',
},
release => {
type => 'string',
description => 'The current Proxmox VE point release in `x.y` format.',
},
repoid => {
type => 'string',
# length 8 is old (< 8.0) short-id, 16 is new short id, 40 is sha1 and 64 is sha256
pattern => '[0-9a-fA-F]{8,64}',
description => 'The short git revision from which this version was build.',
},
console => {
type => 'string',
enum => ['applet', 'vv', 'html5', 'xtermjs'],
optional => 1,
description => 'The default console viewer to use.',
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = {};
my $res = {};
# TODO remove with next major release
my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
for my $k (qw(console)) {
$res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
}
# TODO remove with next major release
my $datacenter_confg = eval { PVE::Cluster::cfs_read_file('datacenter.cfg') } // {};
for my $k (qw(console)) {
$res->{$k} = $datacenter_confg->{$k} if exists $datacenter_confg->{$k};
}
my $version_info = PVE::pvecfg::version_info();
# force set all version keys independent of their definedness
$res->{$_} = $version_info->{$_} for qw(version release repoid);
my $version_info = PVE::pvecfg::version_info();
# force set all version keys independent of their definedness
$res->{$_} = $version_info->{$_} for qw(version release repoid);
return $res;
}});
return $res;
},
});
1;

View file

@ -17,33 +17,34 @@ use base qw(PVE::RESTHandler);
my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "ACME index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return [
{ name => 'certificate' },
];
}});
return [
{ name => 'certificate' },
];
},
});
my $order_certificate = sub {
my ($acme, $acme_node_config) = @_;
@ -51,62 +52,62 @@ my $order_certificate = sub {
my $plugins = PVE::API2::ACMEPlugin::load_config();
print "Placing ACME order\n";
my ($order_url, $order) = $acme->new_order([ keys %{$acme_node_config->{domains}} ]);
my ($order_url, $order) = $acme->new_order([keys %{ $acme_node_config->{domains} }]);
print "Order URL: $order_url\n";
for my $auth_url (@{$order->{authorizations}}) {
print "\nGetting authorization details from '$auth_url'\n";
my $auth = $acme->get_authorization($auth_url);
for my $auth_url (@{ $order->{authorizations} }) {
print "\nGetting authorization details from '$auth_url'\n";
my $auth = $acme->get_authorization($auth_url);
# force lower case, like get_acme_conf does
my $domain = lc($auth->{identifier}->{value});
if ($auth->{status} eq 'valid') {
print "$domain is already validated!\n";
} else {
print "The validation for $domain is pending!\n";
# force lower case, like get_acme_conf does
my $domain = lc($auth->{identifier}->{value});
if ($auth->{status} eq 'valid') {
print "$domain is already validated!\n";
} else {
print "The validation for $domain is pending!\n";
my $domain_config = $acme_node_config->{domains}->{$domain};
die "no config for domain '$domain'\n" if !$domain_config;
my $domain_config = $acme_node_config->{domains}->{$domain};
die "no config for domain '$domain'\n" if !$domain_config;
my $plugin_id = $domain_config->{plugin};
my $plugin_id = $domain_config->{plugin};
my $plugin_cfg = $plugins->{ids}->{$plugin_id};
die "plugin '$plugin_id' for domain '$domain' not found!\n"
if !$plugin_cfg;
my $plugin_cfg = $plugins->{ids}->{$plugin_id};
die "plugin '$plugin_id' for domain '$domain' not found!\n"
if !$plugin_cfg;
my $data = {
plugin => $plugin_cfg,
alias => $domain_config->{alias},
};
my $data = {
plugin => $plugin_cfg,
alias => $domain_config->{alias},
};
my $plugin = PVE::ACME::Challenge->lookup($plugin_cfg->{type});
$plugin->setup($acme, $auth, $data);
my $plugin = PVE::ACME::Challenge->lookup($plugin_cfg->{type});
$plugin->setup($acme, $auth, $data);
print "Triggering validation\n";
eval {
die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n"
if !defined($data->{url});
print "Triggering validation\n";
eval {
die "no validation URL returned by plugin '$plugin_id' for domain '$domain'\n"
if !defined($data->{url});
$acme->request_challenge_validation($data->{url});
print "Sleeping for 5 seconds\n";
sleep 5;
while (1) {
$auth = $acme->get_authorization($auth_url);
if ($auth->{status} eq 'pending') {
print "Status is still 'pending', trying again in 10 seconds\n";
sleep 10;
next;
} elsif ($auth->{status} eq 'valid') {
print "Status is 'valid', domain '$domain' OK!\n";
last;
}
die "validating challenge '$auth_url' failed - status: $auth->{status}\n";
}
};
my $err = $@;
eval { $plugin->teardown($acme, $auth, $data) };
warn "$@\n" if $@;
die $err if $err;
}
$acme->request_challenge_validation($data->{url});
print "Sleeping for 5 seconds\n";
sleep 5;
while (1) {
$auth = $acme->get_authorization($auth_url);
if ($auth->{status} eq 'pending') {
print "Status is still 'pending', trying again in 10 seconds\n";
sleep 10;
next;
} elsif ($auth->{status} eq 'valid') {
print "Status is 'valid', domain '$domain' OK!\n";
last;
}
die "validating challenge '$auth_url' failed - status: $auth->{status}\n";
}
};
my $err = $@;
eval { $plugin->teardown($acme, $auth, $data) };
warn "$@\n" if $@;
die $err if $err;
}
}
print "\nAll domains validated!\n";
print "\nCreating CSR\n";
@ -115,39 +116,37 @@ my $order_certificate = sub {
my $finalize_error_cnt = 0;
print "Checking order status\n";
while (1) {
$order = $acme->get_order($order_url);
if ($order->{status} eq 'pending') {
print "still pending, trying to finalize order\n";
# FIXME
# to be compatible with and without the order ready state we try to
# finalize even at the 'pending' state and give up after 5
# unsuccessful tries this can be removed when the letsencrypt api
# definitely has implemented the 'ready' state
eval {
$acme->finalize_order($order, PVE::Certificate::pem_to_der($csr));
};
if (my $err = $@) {
die $err if $finalize_error_cnt >= 5;
$order = $acme->get_order($order_url);
if ($order->{status} eq 'pending') {
print "still pending, trying to finalize order\n";
# FIXME
# to be compatible with and without the order ready state we try to
# finalize even at the 'pending' state and give up after 5
# unsuccessful tries this can be removed when the letsencrypt api
# definitely has implemented the 'ready' state
eval { $acme->finalize_order($order, PVE::Certificate::pem_to_der($csr)); };
if (my $err = $@) {
die $err if $finalize_error_cnt >= 5;
$finalize_error_cnt++;
warn $err;
}
sleep 5;
next;
} elsif ($order->{status} eq 'ready') {
print "Order is ready, finalizing order\n";
$acme->finalize_order($order, PVE::Certificate::pem_to_der($csr));
sleep 5;
next;
} elsif ($order->{status} eq 'processing') {
print "still processing, trying again in 30 seconds\n";
sleep 30;
next;
} elsif ($order->{status} eq 'valid') {
print "valid!\n";
last;
}
die "order status: $order->{status}\n";
$finalize_error_cnt++;
warn $err;
}
sleep 5;
next;
} elsif ($order->{status} eq 'ready') {
print "Order is ready, finalizing order\n";
$acme->finalize_order($order, PVE::Certificate::pem_to_der($csr));
sleep 5;
next;
} elsif ($order->{status} eq 'processing') {
print "still processing, trying again in 30 seconds\n";
sleep 30;
next;
} elsif ($order->{status} eq 'valid') {
print "valid!\n";
last;
}
die "order status: $order->{status}\n";
}
print "\nDownloading certificate\n";
@ -156,227 +155,235 @@ my $order_certificate = sub {
return ($cert, $key);
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'new_certificate',
path => 'certificate',
method => 'POST',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Order a new certificate from ACME-compatible CA.",
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
type => 'boolean',
description => 'Overwrite existing custom certificate.',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
type => 'boolean',
description => 'Overwrite existing custom certificate.',
optional => 1,
default => 0,
},
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
raise_param_exc({'force' => "Custom certificate exists but 'force' is not set."})
if !$param->{force} && -e "${cert_prefix}.pem";
raise_param_exc({ 'force' => "Custom certificate exists but 'force' is not set." })
if !$param->{force} && -e "${cert_prefix}.pem";
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{$acme_node_config->{domains}};
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{ $acme_node_config->{domains} };
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if ! -e $account_file;
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if !-e $account_file;
my $acme = PVE::ACME->new($account_file);
my $acme = PVE::ACME->new($account_file);
print "Loading ACME account details\n";
$acme->load();
print "Loading ACME account details\n";
$acme->load();
my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
my $code = sub {
print "Setting pveproxy certificate and key\n";
PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, $param->{force});
my $code = sub {
print "Setting pveproxy certificate and key\n";
PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, $param->{force});
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
};
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
};
return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
}});
return $rpcenv->fork_worker("acmenewcert", undef, $authuser, $realcmd);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'renew_certificate',
path => 'certificate',
method => 'PUT',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Renew existing certificate from CA.",
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
type => 'boolean',
description => 'Force renewal even if expiry is more than 30 days away.',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
type => 'boolean',
description => 'Force renewal even if expiry is more than 30 days away.',
optional => 1,
default => 0,
},
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
raise("No current (custom) certificate found, please order a new certificate!\n")
if ! -e "${cert_prefix}.pem";
raise("No current (custom) certificate found, please order a new certificate!\n")
if !-e "${cert_prefix}.pem";
my $expires_soon = PVE::Certificate::check_expiry("${cert_prefix}.pem", time() + 30*24*60*60);
raise_param_exc({'force' => "Certificate does not expire within the next 30 days, and 'force' is not set."})
if !$expires_soon && !$param->{force};
my $expires_soon =
PVE::Certificate::check_expiry("${cert_prefix}.pem", time() + 30 * 24 * 60 * 60);
raise_param_exc(
{
'force' =>
"Certificate does not expire within the next 30 days, and 'force' is not set.",
},
) if !$expires_soon && !$param->{force};
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{$acme_node_config->{domains}};
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{ $acme_node_config->{domains} };
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
my $old_cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
my $old_cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if ! -e $account_file;
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if !-e $account_file;
my $acme = PVE::ACME->new($account_file);
my $acme = PVE::ACME->new($account_file);
print "Loading ACME account details\n";
$acme->load();
print "Loading ACME account details\n";
$acme->load();
my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
my ($cert, $key) = $order_certificate->($acme, $acme_node_config);
my $code = sub {
print "Setting pveproxy certificate and key\n";
PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, 1);
my $code = sub {
print "Setting pveproxy certificate and key\n";
PVE::CertHelpers::set_cert_files($cert, $key, $cert_prefix, 1);
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
print "Revoking old certificate\n";
eval { $acme->revoke_certificate($old_cert) };
warn "Revoke request to CA failed: $@" if $@;
};
print "Revoking old certificate\n";
eval { $acme->revoke_certificate($old_cert) };
warn "Revoke request to CA failed: $@" if $@;
};
return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
}});
return $rpcenv->fork_worker("acmerenew", undef, $authuser, $realcmd);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'revoke_certificate',
path => 'certificate',
method => 'DELETE',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Revoke existing certificate from CA.",
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{$acme_node_config->{domains}};
my $node_config = PVE::NodeConfig::load_config($node);
my $acme_node_config = PVE::NodeConfig::get_acme_conf($node_config);
raise("ACME domain list in node configuration is missing!", 400)
if !$acme_node_config || !%{ $acme_node_config->{domains} };
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
my $cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
my $cert = PVE::Tools::file_get_contents("${cert_prefix}.pem");
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if ! -e $account_file;
my $realcmd = sub {
STDOUT->autoflush(1);
my $account = $acme_node_config->{account};
my $account_file = "${acme_account_dir}/${account}";
die "ACME account config file '$account' does not exist.\n"
if !-e $account_file;
my $acme = PVE::ACME->new($account_file);
my $acme = PVE::ACME->new($account_file);
print "Loading ACME account details\n";
$acme->load();
print "Loading ACME account details\n";
$acme->load();
print "Revoking old certificate\n";
eval { $acme->revoke_certificate($cert) };
if (my $err = $@) {
# is there a better check?
die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/;
}
print "Revoking old certificate\n";
eval { $acme->revoke_certificate($cert) };
if (my $err = $@) {
# is there a better check?
die "Revoke request to CA failed: $err" if $err !~ /"Certificate is expired"/;
}
my $code = sub {
print "Deleting certificate files\n";
unlink "${cert_prefix}.pem";
unlink "${cert_prefix}.key";
my $code = sub {
print "Deleting certificate files\n";
unlink "${cert_prefix}.pem";
unlink "${cert_prefix}.key";
print "Restarting pveproxy to revert to self-signed certificates\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
print "Restarting pveproxy to revert to self-signed certificates\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
};
return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);
}});
return $rpcenv->fork_worker("acmerevoke", undef, $authuser, $realcmd);
},
});
1;

View file

@ -15,61 +15,61 @@ use PVE::API2::ACMEPlugin;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::ACMEPlugin",
path => 'plugins',
});
my $acme_directories = [
{
name => 'Let\'s Encrypt V2',
url => 'https://acme-v02.api.letsencrypt.org/directory',
name => 'Let\'s Encrypt V2',
url => 'https://acme-v02.api.letsencrypt.org/directory',
},
{
name => 'Let\'s Encrypt V2 Staging',
url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
name => 'Let\'s Encrypt V2 Staging',
url => 'https://acme-staging-v02.api.letsencrypt.org/directory',
},
];
my $acme_default_directory_url = $acme_directories->[0]->{url};
my $account_contact_from_param = sub {
my @addresses = PVE::Tools::split_list(extract_param($_[0], 'contact'));
return [ map { "mailto:$_" } @addresses ];
return [map { "mailto:$_" } @addresses];
};
my $acme_account_dir = PVE::CertHelpers::acme_account_dir();
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "ACMEAccount index.",
parameters => {
additionalProperties => 0,
properties => {
},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return [
{ name => 'account' },
{ name => 'tos' },
{ name => 'meta' },
{ name => 'directories' },
{ name => 'plugins' },
{ name => 'challenge-schema' },
];
}});
return [
{ name => 'account' },
{ name => 'tos' },
{ name => 'meta' },
{ name => 'directories' },
{ name => 'plugins' },
{ name => 'challenge-schema' },
];
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'account_index',
path => 'account',
method => 'GET',
@ -77,112 +77,121 @@ __PACKAGE__->register_method ({
description => "ACMEAccount index.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $accounts = PVE::CertHelpers::list_acme_accounts();
return [ map { { name => $_ } } @$accounts ];
}});
my $accounts = PVE::CertHelpers::list_acme_accounts();
return [map { { name => $_ } } @$accounts];
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'register_account',
path => 'account',
method => 'POST',
description => "Register a new ACME account with CA.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact'),
tos_url => {
description => 'URL of CA TermsOfService - setting this indicates agreement.',
type => 'string',
optional => 1,
},
directory => get_standard_option('pve-acme-directory-url', {
default => $acme_default_directory_url,
optional => 1,
}),
'eab-kid' => {
description => 'Key Identifier for External Account Binding.',
type => 'string',
requires => 'eab-hmac-key',
optional => 1,
},
'eab-hmac-key' => {
description => 'HMAC key for External Account Binding.',
type => 'string',
requires => 'eab-kid',
optional => 1,
},
},
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact'),
tos_url => {
description => 'URL of CA TermsOfService - setting this indicates agreement.',
type => 'string',
optional => 1,
},
directory => get_standard_option(
'pve-acme-directory-url',
{
default => $acme_default_directory_url,
optional => 1,
},
),
'eab-kid' => {
description => 'Key Identifier for External Account Binding.',
type => 'string',
requires => 'eab-hmac-key',
optional => 1,
},
'eab-hmac-key' => {
description => 'HMAC key for External Account Binding.',
type => 'string',
requires => 'eab-kid',
optional => 1,
},
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $account_name = extract_param($param, 'name') // 'default';
my $account_file = "${acme_account_dir}/${account_name}";
mkdir $acme_account_dir if ! -e $acme_account_dir;
my $account_name = extract_param($param, 'name') // 'default';
my $account_file = "${acme_account_dir}/${account_name}";
mkdir $acme_account_dir if !-e $acme_account_dir;
my $eab_kid = extract_param($param, 'eab-kid');
my $eab_hmac_key = extract_param($param, 'eab-hmac-key');
my $eab_kid = extract_param($param, 'eab-kid');
my $eab_hmac_key = extract_param($param, 'eab-hmac-key');
raise_param_exc({'name' => "ACME account config file '${account_name}' already exists."})
if -e $account_file;
raise_param_exc({
'name' => "ACME account config file '${account_name}' already exists." })
if -e $account_file;
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $contact = $account_contact_from_param->($param);
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $contact = $account_contact_from_param->($param);
my $realcmd = sub {
PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
die "ACME account config file '${account_name}' already exists.\n"
if -e $account_file;
my $realcmd = sub {
PVE::Cluster::cfs_lock_acme(
$account_name,
10,
sub {
die "ACME account config file '${account_name}' already exists.\n"
if -e $account_file;
my $acme = PVE::ACME->new($account_file, $directory);
print "Generating ACME account key..\n";
$acme->init(4096);
print "Registering ACME account..\n";
my $acme = PVE::ACME->new($account_file, $directory);
print "Generating ACME account key..\n";
$acme->init(4096);
print "Registering ACME account..\n";
my %info = (contact => $contact);
if (defined($eab_kid)) {
$info{eab} = {
kid => $eab_kid,
hmac_key => $eab_hmac_key
};
}
my %info = (contact => $contact);
if (defined($eab_kid)) {
$info{eab} = {
kid => $eab_kid,
hmac_key => $eab_hmac_key,
};
}
eval { $acme->new_account($param->{tos_url}, %info); };
eval { $acme->new_account($param->{tos_url}, %info); };
if (my $err = $@) {
unlink $account_file;
die "Registration failed: $err\n";
}
print "Registration successful, account URL: '$acme->{location}'\n";
});
die $@ if $@;
};
if (my $err = $@) {
unlink $account_file;
die "Registration failed: $err\n";
}
print "Registration successful, account URL: '$acme->{location}'\n";
},
);
die $@ if $@;
};
return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
}});
return $rpcenv->fork_worker('acmeregister', undef, $authuser, $realcmd);
},
});
my $update_account = sub {
my ($param, $msg, %info) = @_;
@ -190,325 +199,347 @@ my $update_account = sub {
my $account_name = extract_param($param, 'name') // 'default';
my $account_file = "${acme_account_dir}/${account_name}";
raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
if ! -e $account_file;
raise_param_exc({ 'name' => "ACME account config file '${account_name}' does not exist." })
if !-e $account_file;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $realcmd = sub {
PVE::Cluster::cfs_lock_acme($account_name, 10, sub {
die "ACME account config file '${account_name}' does not exist.\n"
if ! -e $account_file;
PVE::Cluster::cfs_lock_acme(
$account_name,
10,
sub {
die "ACME account config file '${account_name}' does not exist.\n"
if !-e $account_file;
my $acme = PVE::ACME->new($account_file);
$acme->load();
$acme->update_account(%info);
if ($info{status} && $info{status} eq 'deactivated') {
my $deactivated_name;
for my $i (0..100) {
my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
if (! -e $candidate) {
$deactivated_name = $candidate;
last;
}
}
if ($deactivated_name) {
print "Renaming account file from '$account_file' to '$deactivated_name'\n";
rename($account_file, $deactivated_name) or
warn ".. failed - $!\n";
} else {
warn "No free slot to rename deactivated account file '$account_file', leaving in place\n";
}
}
});
die $@ if $@;
my $acme = PVE::ACME->new($account_file);
$acme->load();
$acme->update_account(%info);
if ($info{status} && $info{status} eq 'deactivated') {
my $deactivated_name;
for my $i (0 .. 100) {
my $candidate = "${acme_account_dir}/_deactivated_${account_name}_${i}";
if (!-e $candidate) {
$deactivated_name = $candidate;
last;
}
}
if ($deactivated_name) {
print
"Renaming account file from '$account_file' to '$deactivated_name'\n";
rename($account_file, $deactivated_name)
or warn ".. failed - $!\n";
} else {
warn
"No free slot to rename deactivated account file '$account_file', leaving in place\n";
}
}
},
);
die $@ if $@;
};
return $rpcenv->fork_worker("acme${msg}", undef, $authuser, $realcmd);
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update_account',
path => 'account/{name}',
method => 'PUT',
description => "Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
description =>
"Update existing ACME account information with CA. Note: not specifying any new account information triggers a refresh.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact', {
optional => 1,
}),
},
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact', {
optional => 1,
}),
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $contact = $account_contact_from_param->($param);
if (scalar @$contact) {
return $update_account->($param, 'update', contact => $contact);
} else {
return $update_account->($param, 'refresh');
}
}});
my $contact = $account_contact_from_param->($param);
if (scalar @$contact) {
return $update_account->($param, 'update', contact => $contact);
} else {
return $update_account->($param, 'refresh');
}
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_account',
path => 'account/{name}',
method => 'GET',
description => "Return existing ACME account information.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
},
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
},
},
returns => {
type => 'object',
additionalProperties => 0,
properties => {
account => {
type => 'object',
optional => 1,
renderer => 'yaml',
},
directory => get_standard_option('pve-acme-directory-url', {
optional => 1,
}),
location => {
type => 'string',
optional => 1,
},
tos => {
type => 'string',
optional => 1,
},
},
type => 'object',
additionalProperties => 0,
properties => {
account => {
type => 'object',
optional => 1,
renderer => 'yaml',
},
directory => get_standard_option('pve-acme-directory-url', {
optional => 1,
}),
location => {
type => 'string',
optional => 1,
},
tos => {
type => 'string',
optional => 1,
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $account_name = extract_param($param, 'name') // 'default';
my $account_file = "${acme_account_dir}/${account_name}";
my $account_name = extract_param($param, 'name') // 'default';
my $account_file = "${acme_account_dir}/${account_name}";
raise_param_exc({'name' => "ACME account config file '${account_name}' does not exist."})
if ! -e $account_file;
raise_param_exc({
'name' => "ACME account config file '${account_name}' does not exist." })
if !-e $account_file;
my $acme = PVE::ACME->new($account_file);
$acme->load();
my $acme = PVE::ACME->new($account_file);
$acme->load();
my $res = {};
$res->{account} = $acme->{account};
$res->{directory} = $acme->{directory};
$res->{location} = $acme->{location};
$res->{tos} = $acme->{tos};
my $res = {};
$res->{account} = $acme->{account};
$res->{directory} = $acme->{directory};
$res->{location} = $acme->{location};
$res->{tos} = $acme->{tos};
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'deactivate_account',
path => 'account/{name}',
method => 'DELETE',
description => "Deactivate existing ACME account at CA.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
},
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return $update_account->($param, 'deactivate', status => 'deactivated');
}});
return $update_account->($param, 'deactivate', status => 'deactivated');
},
});
# TODO: deprecated, remove with pve 9
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_tos',
path => 'tos',
method => 'GET',
description => "Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.",
description =>
"Retrieve ACME TermsOfService URL from CA. Deprecated, please use /cluster/acme/meta.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
directory => get_standard_option('pve-acme-directory-url', {
default => $acme_default_directory_url,
optional => 1,
}),
},
additionalProperties => 0,
properties => {
directory => get_standard_option(
'pve-acme-directory-url',
{
default => $acme_default_directory_url,
optional => 1,
},
),
},
},
returns => {
type => 'string',
optional => 1,
description => 'ACME TermsOfService URL.',
type => 'string',
optional => 1,
description => 'ACME TermsOfService URL.',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $acme = PVE::ACME->new(undef, $directory);
my $meta = $acme->get_meta();
my $acme = PVE::ACME->new(undef, $directory);
my $meta = $acme->get_meta();
return $meta ? $meta->{termsOfService} : undef;
}});
return $meta ? $meta->{termsOfService} : undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_meta',
path => 'meta',
method => 'GET',
description => "Retrieve ACME Directory Meta Information",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
check => ['perm', '/nodes/{node}', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
directory => get_standard_option('pve-acme-directory-url', {
default => $acme_default_directory_url,
optional => 1,
}),
},
additionalProperties => 0,
properties => {
directory => get_standard_option(
'pve-acme-directory-url',
{
default => $acme_default_directory_url,
optional => 1,
},
),
},
},
returns => {
type => 'object',
additionalProperties => 1,
properties => {
termsOfService => {
description => 'ACME TermsOfService URL.',
type => 'string',
optional => 1,
},
externalAccountRequired => {
description => 'EAB Required',
type => 'boolean',
optional => 1,
},
website => {
description => 'URL to more information about the ACME server.',
type => 'string',
optional => 1,
},
caaIdentities => {
description => 'Hostnames referring to the ACME servers.',
type => 'array',
items => {
type => 'string',
},
optional => 1,
},
},
type => 'object',
additionalProperties => 1,
properties => {
termsOfService => {
description => 'ACME TermsOfService URL.',
type => 'string',
optional => 1,
},
externalAccountRequired => {
description => 'EAB Required',
type => 'boolean',
optional => 1,
},
website => {
description => 'URL to more information about the ACME server.',
type => 'string',
optional => 1,
},
caaIdentities => {
description => 'Hostnames referring to the ACME servers.',
type => 'array',
items => {
type => 'string',
},
optional => 1,
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $directory = extract_param($param, 'directory') // $acme_default_directory_url;
my $acme = PVE::ACME->new(undef, $directory);
my $meta = $acme->get_meta();
my $acme = PVE::ACME->new(undef, $directory);
my $meta = $acme->get_meta();
return $meta;
}});
return $meta;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_directories',
path => 'directories',
method => 'GET',
description => "Get named known ACME directory endpoints.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => 'object',
additionalProperties => 0,
properties => {
name => {
type => 'string',
},
url => get_standard_option('pve-acme-directory-url'),
},
},
type => 'array',
items => {
type => 'object',
additionalProperties => 0,
properties => {
name => {
type => 'string',
},
url => get_standard_option('pve-acme-directory-url'),
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return $acme_directories;
}});
return $acme_directories;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'challengeschema',
path => 'challenge-schema',
method => 'GET',
description => "Get schema of ACME challenge types.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => 'object',
additionalProperties => 0,
properties => {
id => {
type => 'string',
},
name => {
description => 'Human readable name, falls back to id',
type => 'string',
},
type => {
type => 'string',
},
schema => {
type => 'object',
},
},
},
type => 'array',
items => {
type => 'object',
additionalProperties => 0,
properties => {
id => {
type => 'string',
},
name => {
description => 'Human readable name, falls back to id',
type => 'string',
},
type => {
type => 'string',
},
schema => {
type => 'object',
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
my $res = [];
my $res = [];
for my $type (@$plugin_type_enum) {
my $plugin = PVE::ACME::Challenge->lookup($type);
next if !$plugin->can('get_supported_plugins');
for my $type (@$plugin_type_enum) {
my $plugin = PVE::ACME::Challenge->lookup($type);
next if !$plugin->can('get_supported_plugins');
my $plugin_type = $plugin->type();
my $plugins = $plugin->get_supported_plugins();
for my $id (sort keys %$plugins) {
my $schema = $plugins->{$id};
push @$res, {
id => $id,
name => $schema->{name} // $id,
type => $plugin_type,
schema => $schema,
};
}
}
my $plugin_type = $plugin->type();
my $plugins = $plugin->get_supported_plugins();
for my $id (sort keys %$plugins) {
my $schema = $plugins->{$id};
push @$res,
{
id => $id,
name => $schema->{name} // $id,
type => $plugin_type,
schema => $schema,
};
}
}
return $res;
}});
return $res;
},
});
1;

View file

@ -17,20 +17,24 @@ use base qw(PVE::RESTHandler);
my $plugin_config_file = "priv/acme/plugins.cfg";
cfs_register_file($plugin_config_file,
sub { PVE::ACME::Challenge->parse_config(@_); },
sub { PVE::ACME::Challenge->write_config(@_); },
cfs_register_file(
$plugin_config_file,
sub { PVE::ACME::Challenge->parse_config(@_); },
sub { PVE::ACME::Challenge->write_config(@_); },
);
PVE::ACME::DNSChallenge->register();
PVE::ACME::StandAlone->register();
PVE::ACME::Challenge->init();
PVE::JSONSchema::register_standard_option('pve-acme-pluginid', {
type => 'string',
format => 'pve-configid',
description => 'Unique identifier for ACME plugin instance.',
});
PVE::JSONSchema::register_standard_option(
'pve-acme-pluginid',
{
type => 'string',
format => 'pve-configid',
description => 'Unique identifier for ACME plugin instance.',
},
);
my $plugin_type_enum = PVE::ACME::Challenge->lookup_types();
@ -46,50 +50,50 @@ my $modify_cfg_for_api = sub {
return $plugin_cfg;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
description => "ACME plugin index.",
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
type => {
description => "Only list ACME plugins of a specific type",
type => 'string',
enum => $plugin_type_enum,
optional => 1,
},
},
additionalProperties => 0,
properties => {
type => {
description => "Only list ACME plugins of a specific type",
type => 'string',
enum => $plugin_type_enum,
optional => 1,
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
plugin => get_standard_option('pve-acme-pluginid'),
},
},
links => [ { rel => 'child', href => "{plugin}" } ],
type => 'array',
items => {
type => "object",
properties => {
plugin => get_standard_option('pve-acme-pluginid'),
},
},
links => [{ rel => 'child', href => "{plugin}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cfg = load_config();
my $cfg = load_config();
my $res = [];
foreach my $pluginid (keys %{$cfg->{ids}}) {
my $plugin_cfg = $modify_cfg_for_api->($cfg, $pluginid);
next if $param->{type} && $param->{type} ne $plugin_cfg->{type};
push @$res, $plugin_cfg;
}
my $res = [];
foreach my $pluginid (keys %{ $cfg->{ids} }) {
my $plugin_cfg = $modify_cfg_for_api->($cfg, $pluginid);
next if $param->{type} && $param->{type} ne $plugin_cfg->{type};
push @$res, $plugin_cfg;
}
return $res;
}
return $res;
},
});
__PACKAGE__->register_method({
@ -98,24 +102,24 @@ __PACKAGE__->register_method({
method => 'GET',
description => "Get ACME plugin configuration.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-acme-pluginid'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-acme-pluginid'),
},
},
returns => {
type => 'object',
type => 'object',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cfg = load_config();
return $modify_cfg_for_api->($cfg, $param->{id});
}
my $cfg = load_config();
return $modify_cfg_for_api->($cfg, $param->{id});
},
});
__PACKAGE__->register_method({
@ -124,35 +128,39 @@ __PACKAGE__->register_method({
method => 'POST',
description => "Add ACME plugin configuration.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
protected => 1,
parameters => PVE::ACME::Challenge->createSchema(),
returns => {
type => "null"
type => "null",
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $type = extract_param($param, 'type');
my $id = extract_param($param, 'id');
my $type = extract_param($param, 'type');
cfs_lock_file($plugin_config_file, undef, sub {
my $cfg = load_config();
die "ACME plugin ID '$id' already exists\n" if defined($cfg->{ids}->{$id});
cfs_lock_file(
$plugin_config_file,
undef,
sub {
my $cfg = load_config();
die "ACME plugin ID '$id' already exists\n" if defined($cfg->{ids}->{$id});
my $plugin = PVE::ACME::Challenge->lookup($type);
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::ACME::Challenge->lookup($type);
my $opts = $plugin->check_config($id, $param, 1, 1);
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id}->{type} = $type;
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id}->{type} = $type;
cfs_write_file($plugin_config_file, $cfg);
});
die "$@" if $@;
cfs_write_file($plugin_config_file, $cfg);
},
);
die "$@" if $@;
return undef;
}
return undef;
},
});
__PACKAGE__->register_method({
@ -161,51 +169,55 @@ __PACKAGE__->register_method({
method => 'PUT',
description => "Update ACME plugin configuration.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
protected => 1,
parameters => PVE::ACME::Challenge->updateSchema(),
returns => {
type => "null"
type => "null",
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $delete = extract_param($param, 'delete');
my $digest = extract_param($param, 'digest');
my $id = extract_param($param, 'id');
my $delete = extract_param($param, 'delete');
my $digest = extract_param($param, 'digest');
cfs_lock_file($plugin_config_file, undef, sub {
my $cfg = load_config();
PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
my $plugin_cfg = $cfg->{ids}->{$id};
die "ACME plugin ID '$id' does not exist\n" if !$plugin_cfg;
cfs_lock_file(
$plugin_config_file,
undef,
sub {
my $cfg = load_config();
PVE::Tools::assert_if_modified($cfg->{digest}, $digest);
my $plugin_cfg = $cfg->{ids}->{$id};
die "ACME plugin ID '$id' does not exist\n" if !$plugin_cfg;
my $type = $plugin_cfg->{type};
my $plugin = PVE::ACME::Challenge->lookup($type);
my $type = $plugin_cfg->{type};
my $plugin = PVE::ACME::Challenge->lookup($type);
if (defined($delete)) {
my $schema = $plugin->private();
my $options = $schema->{options}->{$type};
for my $k (PVE::Tools::split_list($delete)) {
my $d = $options->{$k} || die "no such option '$k'\n";
die "unable to delete required option '$k'\n" if !$d->{optional};
if (defined($delete)) {
my $schema = $plugin->private();
my $options = $schema->{options}->{$type};
for my $k (PVE::Tools::split_list($delete)) {
my $d = $options->{$k} || die "no such option '$k'\n";
die "unable to delete required option '$k'\n" if !$d->{optional};
delete $cfg->{ids}->{$id}->{$k};
}
}
delete $cfg->{ids}->{$id}->{$k};
}
}
my $opts = $plugin->check_config($id, $param, 0, 1);
for my $k (sort keys %$opts) {
$plugin_cfg->{$k} = $opts->{$k};
}
my $opts = $plugin->check_config($id, $param, 0, 1);
for my $k (sort keys %$opts) {
$plugin_cfg->{$k} = $opts->{$k};
}
cfs_write_file($plugin_config_file, $cfg);
});
die "$@" if $@;
cfs_write_file($plugin_config_file, $cfg);
},
);
die "$@" if $@;
return undef;
}
return undef;
},
});
__PACKAGE__->register_method({
@ -214,34 +226,38 @@ __PACKAGE__->register_method({
method => 'DELETE',
description => "Delete ACME plugin configuration.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-acme-pluginid'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-acme-pluginid'),
},
},
returns => {
type => "null"
type => "null",
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $id = extract_param($param, 'id');
cfs_lock_file($plugin_config_file, undef, sub {
my $cfg = load_config();
cfs_lock_file(
$plugin_config_file,
undef,
sub {
my $cfg = load_config();
delete $cfg->{ids}->{$id};
delete $cfg->{ids}->{$id};
cfs_write_file($plugin_config_file, $cfg);
});
die "$@" if $@;
cfs_write_file($plugin_config_file, $cfg);
},
);
die "$@" if $@;
return undef;
}
return undef;
},
});
sub load_config {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -10,79 +10,76 @@ use PVE::API2::Qemu::Machine;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Qemu::CPU",
path => 'qemu/cpu',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Qemu::Machine",
path => 'qemu/machines',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Node capabilities index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'qemu' },
];
my $result = [
{ name => 'qemu' },
];
return $result;
}
return $result;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'qemu_caps_index',
path => 'qemu',
method => 'GET',
permissions => { user => 'all' },
description => "QEMU capabilities index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'cpu' },
{ name => 'machines' },
];
my $result = [
{ name => 'cpu' }, { name => 'machines' },
];
return $result;
}
return $result;
},
});
1;

File diff suppressed because it is too large Load diff

View file

@ -11,184 +11,185 @@ use PVE::Tools qw(file_get_contents);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "Directory index.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'raw' },
{ name => 'db' },
{ name => 'value' },
];
my $result = [
{ name => 'raw' }, { name => 'db' }, { name => 'value' },
];
return $result;
}});
return $result;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'raw',
path => 'raw',
method => 'GET',
proxyto => 'node',
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
description => "Get the Ceph configuration file.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $path = PVE::Ceph::Tools::get_config('pve_ceph_cfgpath');
return file_get_contents($path);
my $path = PVE::Ceph::Tools::get_config('pve_ceph_cfgpath');
return file_get_contents($path);
}});
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'db',
path => 'db',
method => 'GET',
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
description => "Get the Ceph configuration database.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => 'object',
properties => {
section => { type => "string", },
name => { type => "string", },
value => { type => "string", },
level => { type => "string", },
'can_update_at_runtime' => { type => "boolean", },
mask => { type => "string" },
},
},
type => 'array',
items => {
type => 'object',
properties => {
section => { type => "string" },
name => { type => "string" },
value => { type => "string" },
level => { type => "string" },
'can_update_at_runtime' => { type => "boolean" },
mask => { type => "string" },
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $rados = PVE::RADOS->new();
my $res = $rados->mon_command( { prefix => 'config dump', format => 'json' });
foreach my $entry (@$res) {
$entry->{can_update_at_runtime} = $entry->{can_update_at_runtime}? 1 : 0; # JSON::true/false -> 1/0
}
return $res;
}});
my $rados = PVE::RADOS->new();
my $res = $rados->mon_command({ prefix => 'config dump', format => 'json' });
foreach my $entry (@$res) {
$entry->{can_update_at_runtime} = $entry->{can_update_at_runtime} ? 1 : 0; # JSON::true/false -> 1/0
}
return $res;
},
});
my $SINGLE_CONFIGKEY_RE = qr/[0-9a-z\-_\.]+:[0-9a-zA-Z\-_]+/i;
my $CONFIGKEYS_RE = qr/^(:?${SINGLE_CONFIGKEY_RE})(:?[;, ]${SINGLE_CONFIGKEY_RE})*$/;
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'value',
path => 'value',
method => 'GET',
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
check => ['perm', '/', ['Sys.Audit']],
},
description => "Get configured values from either the config file or config DB.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'config-keys' => {
type => "string",
typetext => "<section>:<config key>[;<section>:<config key>]",
pattern => $CONFIGKEYS_RE,
description => "List of <section>:<config key> items.",
}
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'config-keys' => {
type => "string",
typetext => "<section>:<config key>[;<section>:<config key>]",
pattern => $CONFIGKEYS_RE,
description => "List of <section>:<config key> items.",
},
},
},
returns => {
type => 'object',
description => "Contains {section}->{key} children with the values",
type => 'object',
description => "Contains {section}->{key} children with the values",
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
# Ceph treats '-' and '_' the same in parameter names, stick with '-'
my $normalize = sub {
my $t = shift;
$t =~ s/_/-/g;
return $t;
};
# Ceph treats '-' and '_' the same in parameter names, stick with '-'
my $normalize = sub {
my $t = shift;
$t =~ s/_/-/g;
return $t;
};
my $requested_keys = {};
for my $pair (PVE::Tools::split_list($param->{'config-keys'})) {
my ($section, $key) = split(":", $pair);
$section = $normalize->($section);
$key = $normalize->($key);
my $requested_keys = {};
for my $pair (PVE::Tools::split_list($param->{'config-keys'})) {
my ($section, $key) = split(":", $pair);
$section = $normalize->($section);
$key = $normalize->($key);
$requested_keys->{$section}->{$key} = 1;
}
$requested_keys->{$section}->{$key} = 1;
}
my $config = {};
my $config = {};
my $rados = PVE::RADOS->new();
my $configdb = $rados->mon_command( { prefix => 'config dump', format => 'json' });
for my $s (@{$configdb}) {
my ($section, $name, $value) = $s->@{'section', 'name', 'value'};
my $n_section = $normalize->($section);
my $n_name = $normalize->($name);
my $rados = PVE::RADOS->new();
my $configdb = $rados->mon_command({ prefix => 'config dump', format => 'json' });
for my $s (@{$configdb}) {
my ($section, $name, $value) = $s->@{ 'section', 'name', 'value' };
my $n_section = $normalize->($section);
my $n_name = $normalize->($name);
$config->{$n_section}->{$n_name} = $value
if defined $requested_keys->{$n_section} && $n_name eq $n_name;
}
$config->{$n_section}->{$n_name} = $value
if defined $requested_keys->{$n_section} && $n_name eq $n_name;
}
# read ceph.conf after config db as it has priority if settings are present in both
my $config_file = cfs_read_file('ceph.conf'); # cfs_read_file to get it parsed
for my $section (keys %{$config_file}) {
my $n_section = $normalize->($section);
next if !defined $requested_keys->{$n_section};
# read ceph.conf after config db as it has priority if settings are present in both
my $config_file = cfs_read_file('ceph.conf'); # cfs_read_file to get it parsed
for my $section (keys %{$config_file}) {
my $n_section = $normalize->($section);
next if !defined $requested_keys->{$n_section};
for my $key (keys %{$config_file->{$section}}) {
my $n_key = $normalize->($key);
$config->{$n_section}->{$n_key} = $config_file->{$section}->{$key}
if $requested_keys->{$n_section}->{$n_key};
}
}
for my $key (keys %{ $config_file->{$section} }) {
my $n_key = $normalize->($key);
$config->{$n_section}->{$n_key} = $config_file->{$section}->{$key}
if $requested_keys->{$n_section}->{$n_key};
}
}
return $config;
}});
return $config;
},
});

View file

@ -15,65 +15,65 @@ use PVE::API2::Storage::Config;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
proxyto => 'node',
description => "Directory index.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The ceph filesystem name.",
type => 'string',
},
metadata_pool => {
description => "The name of the metadata pool.",
type => 'string',
},
data_pool => {
description => "The name of the data pool.",
type => 'string',
},
},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The ceph filesystem name.",
type => 'string',
},
metadata_pool => {
description => "The name of the metadata pool.",
type => 'string',
},
data_pool => {
description => "The name of the data pool.",
type => 'string',
},
},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $rados = PVE::RADOS->new();
my $rados = PVE::RADOS->new();
my $cephfs_list = PVE::Ceph::Tools::ls_fs($rados);
my $cephfs_list = PVE::Ceph::Tools::ls_fs($rados);
my $res = [
map {{
name => $_->{name},
metadata_pool => $_->{metadata_pool},
data_pool => $_->{data_pools}->[0],
}} @$cephfs_list
];
my $res = [
map { {
name => $_->{name},
metadata_pool => $_->{metadata_pool},
data_pool => $_->{data_pools}->[0],
} } @$cephfs_list
];
return $res;
}
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'createfs',
path => '{name}',
method => 'POST',
@ -81,141 +81,143 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => "The ceph filesystem name.",
type => 'string',
default => 'cephfs',
optional => 1,
pattern => qr|^[^:/\s]+$|,
},
pg_num => {
description => "Number of placement groups for the backing data pool. The metadata pool will use a quarter of this.",
type => 'integer',
default => 128,
optional => 1,
minimum => 8,
maximum => 32768,
},
'add-storage' => {
description => "Configure the created CephFS as storage for this cluster.",
type => 'boolean',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => "The ceph filesystem name.",
type => 'string',
default => 'cephfs',
optional => 1,
pattern => qr|^[^:/\s]+$|,
},
pg_num => {
description =>
"Number of placement groups for the backing data pool. The metadata pool will use a quarter of this.",
type => 'integer',
default => 128,
optional => 1,
minimum => 8,
maximum => 32768,
},
'add-storage' => {
description => "Configure the created CephFS as storage for this cluster.",
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_configured();
PVE::Ceph::Tools::check_ceph_configured();
my $fs_name = $param->{name} // 'cephfs';
my $pg_num = $param->{pg_num} // 128;
my $fs_name = $param->{name} // 'cephfs';
my $pg_num = $param->{pg_num} // 128;
my $pool_data = "${fs_name}_data";
my $pool_metadata = "${fs_name}_metadata";
my $pool_data = "${fs_name}_data";
my $pool_metadata = "${fs_name}_metadata";
my $rados = PVE::RADOS->new();
my $ls_pools = PVE::Ceph::Tools::ls_pools();
my $existing_pools = { map { $_->{poolname} => 1 } @$ls_pools };
my $rados = PVE::RADOS->new();
my $ls_pools = PVE::Ceph::Tools::ls_pools();
my $existing_pools = { map { $_->{poolname} => 1 } @$ls_pools };
die "ceph pools '$pool_data' and/or '$pool_metadata' already exist\n"
if $existing_pools->{$pool_data} || $existing_pools->{$pool_metadata};
die "ceph pools '$pool_data' and/or '$pool_metadata' already exist\n"
if $existing_pools->{$pool_data} || $existing_pools->{$pool_metadata};
my $fs = PVE::Ceph::Tools::ls_fs($rados);
die "ceph fs '$fs_name' already exists\n"
if (grep { $_->{name} eq $fs_name } @$fs);
my $fs = PVE::Ceph::Tools::ls_fs($rados);
die "ceph fs '$fs_name' already exists\n"
if (grep { $_->{name} eq $fs_name } @$fs);
my $running_mds = PVE::Ceph::Services::get_cluster_mds_state($rados);
die "no running Metadata Server (MDS) found!\n" if !scalar(keys %$running_mds);
die "no standby Metadata Server (MDS) found!\n"
if !grep { $_->{state} eq 'up:standby' } values(%$running_mds);
my $running_mds = PVE::Ceph::Services::get_cluster_mds_state($rados);
die "no running Metadata Server (MDS) found!\n" if !scalar(keys %$running_mds);
die "no standby Metadata Server (MDS) found!\n"
if !grep { $_->{state} eq 'up:standby' } values(%$running_mds);
PVE::Storage::assert_sid_unused($fs_name) if $param->{add_storage};
PVE::Storage::assert_sid_unused($fs_name) if $param->{add_storage};
my $worker = sub {
$rados = PVE::RADOS->new();
my $worker = sub {
$rados = PVE::RADOS->new();
my $pool_param = {
application => 'cephfs',
pg_num => $pg_num,
};
my $pool_param = {
application => 'cephfs',
pg_num => $pg_num,
};
my @created_pools = ();
eval {
print "creating data pool '$pool_data'...\n";
PVE::Ceph::Tools::create_pool($pool_data, $pool_param, $rados);
push @created_pools, $pool_data;
my @created_pools = ();
eval {
print "creating data pool '$pool_data'...\n";
PVE::Ceph::Tools::create_pool($pool_data, $pool_param, $rados);
push @created_pools, $pool_data;
print "creating metadata pool '$pool_metadata'...\n";
$pool_param->{pg_num} = $pg_num >= 32 ? $pg_num / 4 : 8;
PVE::Ceph::Tools::create_pool($pool_metadata, $pool_param, $rados);
push @created_pools, $pool_metadata;
print "creating metadata pool '$pool_metadata'...\n";
$pool_param->{pg_num} = $pg_num >= 32 ? $pg_num / 4 : 8;
PVE::Ceph::Tools::create_pool($pool_metadata, $pool_param, $rados);
push @created_pools, $pool_metadata;
print "configuring new CephFS '$fs_name'\n";
my $param = {
pool_metadata => $pool_metadata,
pool_data => $pool_data,
};
PVE::Ceph::Tools::create_fs($fs_name, $param, $rados);
};
if (my $err = $@) {
$@ = undef;
print "configuring new CephFS '$fs_name'\n";
my $param = {
pool_metadata => $pool_metadata,
pool_data => $pool_data,
};
PVE::Ceph::Tools::create_fs($fs_name, $param, $rados);
};
if (my $err = $@) {
$@ = undef;
if (@created_pools > 0) {
warn "Encountered error after creating at least one pool\n";
# our old connection is very likely broken now, recreate
$rados = PVE::RADOS->new();
foreach my $pool (@created_pools) {
warn "cleaning up left over pool '$pool'\n";
eval { PVE::Ceph::Tools::destroy_pool($pool, $rados) };
warn "$@\n" if $@;
}
}
if (@created_pools > 0) {
warn "Encountered error after creating at least one pool\n";
# our old connection is very likely broken now, recreate
$rados = PVE::RADOS->new();
foreach my $pool (@created_pools) {
warn "cleaning up left over pool '$pool'\n";
eval { PVE::Ceph::Tools::destroy_pool($pool, $rados) };
warn "$@\n" if $@;
}
}
die "$err\n";
}
print "Successfully create CephFS '$fs_name'\n";
die "$err\n";
}
print "Successfully create CephFS '$fs_name'\n";
if ($param->{'add-storage'}) {
print "Adding '$fs_name' to storage configuration...\n";
if ($param->{'add-storage'}) {
print "Adding '$fs_name' to storage configuration...\n";
my $waittime = 0;
while (!PVE::Ceph::Services::is_mds_active($rados, $fs_name)) {
if ($waittime >= 10) {
die "Need MDS to add storage, but none got active!\n";
}
my $waittime = 0;
while (!PVE::Ceph::Services::is_mds_active($rados, $fs_name)) {
if ($waittime >= 10) {
die "Need MDS to add storage, but none got active!\n";
}
print "Waiting for an MDS to become active\n";
sleep(1);
$waittime++;
}
print "Waiting for an MDS to become active\n";
sleep(1);
$waittime++;
}
eval {
PVE::API2::Storage::Config->create({
type => 'cephfs',
storage => $fs_name,
content => 'backup,iso,vztmpl',
'fs-name' => $fs_name,
})
};
die "adding storage for CephFS '$fs_name' failed, check log ".
"and add manually!\n$@\n" if $@;
}
};
eval {
PVE::API2::Storage::Config->create({
type => 'cephfs',
storage => $fs_name,
content => 'backup,iso,vztmpl',
'fs-name' => $fs_name,
});
};
die "adding storage for CephFS '$fs_name' failed, check log "
. "and add manually!\n$@\n"
if $@;
}
};
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
return $rpcenv->fork_worker('cephfscreate', $fs_name, $user, $worker);
}
return $rpcenv->fork_worker('cephfscreate', $fs_name, $user, $worker);
},
});
1;

View file

@ -14,79 +14,80 @@ use PVE::RPCEnvironment;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "MDS directory index.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The name (ID) for the MDS",
},
addr => {
type => 'string',
optional => 1,
},
host => {
type => 'string',
optional => 1,
},
state => {
type => 'string',
description => 'State of the MDS',
},
standby_replay => {
type => 'boolean',
optional => 1,
description => 'If true, the standby MDS is polling the active MDS for faster recovery (hot standby).',
},
rank => {
type => 'integer',
optional => 1,
},
},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The name (ID) for the MDS",
},
addr => {
type => 'string',
optional => 1,
},
host => {
type => 'string',
optional => 1,
},
state => {
type => 'string',
description => 'State of the MDS',
},
standby_replay => {
type => 'boolean',
optional => 1,
description =>
'If true, the standby MDS is polling the active MDS for faster recovery (hot standby).',
},
rank => {
type => 'integer',
optional => 1,
},
},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $res = [];
my $res = [];
my $cfg = cfs_read_file('ceph.conf');
my $rados = PVE::RADOS->new();
my $cfg = cfs_read_file('ceph.conf');
my $rados = PVE::RADOS->new();
my $mds_hash = PVE::Ceph::Services::get_services_info("mds", $cfg, $rados);
my $mds_hash = PVE::Ceph::Services::get_services_info("mds", $cfg, $rados);
my $mds_state = PVE::Ceph::Services::get_cluster_mds_state($rados);
foreach my $name (keys %$mds_state) {
my $d = $mds_state->{$name};
# just overwrite, this always provides more info
$mds_hash->{$name}->{$_} = $d->{$_} for keys %$d;
}
my $mds_state = PVE::Ceph::Services::get_cluster_mds_state($rados);
foreach my $name (keys %$mds_state) {
my $d = $mds_state->{$name};
# just overwrite, this always provides more info
$mds_hash->{$name}->{$_} = $d->{$_} for keys %$d;
}
return PVE::RESTHandler::hash_to_array($mds_hash, 'name');
}
return PVE::RESTHandler::hash_to_array($mds_hash, 'name');
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'createmds',
path => '{name}',
method => 'POST',
@ -94,94 +95,95 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
type => 'string',
optional => 1,
default => 'nodename',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the mds, when omitted the same as the nodename",
},
hotstandby => {
type => 'boolean',
optional => 1,
default => '0',
description => "Determines whether a ceph-mds daemon should poll and replay the log of an active MDS. ".
"Faster switch on MDS failure, but needs more idle resources.",
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
type => 'string',
optional => 1,
default => 'nodename',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the mds, when omitted the same as the nodename",
},
hotstandby => {
type => 'boolean',
optional => 1,
default => '0',
description =>
"Determines whether a ceph-mds daemon should poll and replay the log of an active MDS. "
. "Faster switch on MDS failure, but needs more idle resources.",
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_installed('ceph_mds');
PVE::Ceph::Tools::check_ceph_installed('ceph_mds');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $nodename = $param->{node};
$nodename = INotify::nodename() if $nodename eq 'localhost';
my $nodename = $param->{node};
$nodename = INotify::nodename() if $nodename eq 'localhost';
my $mds_id = $param->{name} // $nodename;
my $mds_id = $param->{name} // $nodename;
die "ID of the MDS cannot start with a number!\n" if ($mds_id =~ /^[0-9]/);
die "ID of the MDS cannot start with a number!\n" if ($mds_id =~ /^[0-9]/);
my $worker = sub {
my $timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $timeout);
my $worker = sub {
my $timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $timeout);
my $cfg = cfs_read_file('ceph.conf');
my $cfg = cfs_read_file('ceph.conf');
my $section = "mds.$mds_id";
my $section = "mds.$mds_id";
if (defined($cfg->{$section})) {
die "MDS '$mds_id' already referenced in ceph config, abort!\n"
}
if (defined($cfg->{$section})) {
die "MDS '$mds_id' already referenced in ceph config, abort!\n";
}
if (!defined($cfg->{mds}->{keyring})) {
# $id isn't a perl variable but a ceph metavariable
my $keyring = '/var/lib/ceph/mds/ceph-$id/keyring';
if (!defined($cfg->{mds}->{keyring})) {
# $id isn't a perl variable but a ceph metavariable
my $keyring = '/var/lib/ceph/mds/ceph-$id/keyring';
$cfg->{mds}->{keyring} = $keyring;
}
$cfg->{mds}->{keyring} = $keyring;
}
$cfg->{$section}->{host} = $nodename;
$cfg->{$section}->{'mds_standby_for_name'} = 'pve';
$cfg->{$section}->{host} = $nodename;
$cfg->{$section}->{'mds_standby_for_name'} = 'pve';
if ($param->{hotstandby}) {
$cfg->{$section}->{'mds_standby_replay'} = 'true';
}
if ($param->{hotstandby}) {
$cfg->{$section}->{'mds_standby_replay'} = 'true';
}
cfs_write_file('ceph.conf', $cfg);
cfs_write_file('ceph.conf', $cfg);
eval { PVE::Ceph::Services::create_mds($mds_id, $rados) };
if (my $err = $@) {
# we abort early if the section is defined, so we know that we
# wrote it at this point. Do not auto remove the service, could
# do real harm for previously manual setup MDS
warn "Encountered error, remove '$section' from ceph.conf\n";
my $cfg = cfs_read_file('ceph.conf');
delete $cfg->{$section};
cfs_write_file('ceph.conf', $cfg);
eval { PVE::Ceph::Services::create_mds($mds_id, $rados) };
if (my $err = $@) {
# we abort early if the section is defined, so we know that we
# wrote it at this point. Do not auto remove the service, could
# do real harm for previously manual setup MDS
warn "Encountered error, remove '$section' from ceph.conf\n";
my $cfg = cfs_read_file('ceph.conf');
delete $cfg->{$section};
cfs_write_file('ceph.conf', $cfg);
die "$err\n";
}
};
die "$err\n";
}
};
return $rpcenv->fork_worker('cephcreatemds', "mds.$mds_id", $authuser, $worker);
}
return $rpcenv->fork_worker('cephcreatemds', "mds.$mds_id", $authuser, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'destroymds',
path => '{name}',
method => 'DELETE',
@ -189,47 +191,47 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => 'The name (ID) of the mds',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => 'The name (ID) of the mds',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $mds_id = $param->{name};
my $mds_id = $param->{name};
my $worker = sub {
my $timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $timeout);
my $worker = sub {
my $timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $timeout);
my $cfg = cfs_read_file('ceph.conf');
my $cfg = cfs_read_file('ceph.conf');
if (defined($cfg->{"mds.$mds_id"})) {
delete $cfg->{"mds.$mds_id"};
cfs_write_file('ceph.conf', $cfg);
}
if (defined($cfg->{"mds.$mds_id"})) {
delete $cfg->{"mds.$mds_id"};
cfs_write_file('ceph.conf', $cfg);
}
PVE::Ceph::Services::destroy_mds($mds_id, $rados);
};
PVE::Ceph::Services::destroy_mds($mds_id, $rados);
};
return $rpcenv->fork_worker('cephdestroymds', "mds.$mds_id", $authuser, $worker);
}
return $rpcenv->fork_worker('cephdestroymds', "mds.$mds_id", $authuser, $worker);
},
});
1;

View file

@ -15,72 +15,72 @@ use PVE::Tools qw(run_command);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "MGR directory index.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The name (ID) for the MGR",
},
addr => {
type => 'string',
optional => 1,
},
host => {
type => 'string',
optional => 1,
},
state => {
type => 'string',
description => 'State of the MGR',
},
},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {
name => {
description => "The name (ID) for the MGR",
},
addr => {
type => 'string',
optional => 1,
},
host => {
type => 'string',
optional => 1,
},
state => {
type => 'string',
description => 'State of the MGR',
},
},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $res = [];
my $res = [];
my $cfg = cfs_read_file('ceph.conf');
my $rados = PVE::RADOS->new();
my $cfg = cfs_read_file('ceph.conf');
my $rados = PVE::RADOS->new();
my $mgr_hash = PVE::Ceph::Services::get_services_info("mgr", $cfg, $rados);
my $mgr_hash = PVE::Ceph::Services::get_services_info("mgr", $cfg, $rados);
my $mgr_dump = $rados->mon_command({ prefix => 'mgr dump' });
my $mgr_dump = $rados->mon_command({ prefix => 'mgr dump' });
my $active_name = $mgr_dump->{active_name};
$mgr_hash->{$active_name}->{state} = 'active' if $active_name;
my $active_name = $mgr_dump->{active_name};
$mgr_hash->{$active_name}->{state} = 'active' if $active_name;
foreach my $mgr (@{$mgr_dump->{standbys}}) {
$mgr_hash->{$mgr->{name}}->{state} = 'standby';
}
foreach my $mgr (@{ $mgr_dump->{standbys} }) {
$mgr_hash->{ $mgr->{name} }->{state} = 'standby';
}
return PVE::RESTHandler::hash_to_array($mgr_hash, 'name');
}
return PVE::RESTHandler::hash_to_array($mgr_hash, 'name');
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'createmgr',
path => '{id}',
method => 'POST',
@ -88,47 +88,48 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
id => {
type => 'string',
optional => 1,
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the manager, when omitted the same as the nodename",
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
id => {
type => 'string',
optional => 1,
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the manager, when omitted the same as the nodename",
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_installed('ceph_mgr');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::setup_pve_symlinks();
PVE::Ceph::Tools::check_ceph_installed('ceph_mgr');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::setup_pve_symlinks();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $mgrid = $param->{id} // $param->{node};
my $mgrid = $param->{id} // $param->{node};
my $worker = sub {
my $upid = shift;
my $worker = sub {
my $upid = shift;
my $rados_timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $rados_timeout);
my $rados_timeout = PVE::Ceph::Tools::get_config('long_rados_timeout');
my $rados = PVE::RADOS->new(timeout => $rados_timeout);
PVE::Ceph::Services::create_mgr($mgrid, $rados);
};
PVE::Ceph::Services::create_mgr($mgrid, $rados);
};
return $rpcenv->fork_worker('cephcreatemgr', "mgr.$mgrid", $authuser, $worker);
}});
return $rpcenv->fork_worker('cephcreatemgr', "mgr.$mgrid", $authuser, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'destroymgr',
path => '{id}',
method => 'DELETE',
@ -136,36 +137,37 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
id => {
description => 'The ID of the manager',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
id => {
description => 'The ID of the manager',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $mgrid = $param->{id};
my $mgrid = $param->{id};
my $worker = sub {
my $upid = shift;
my $worker = sub {
my $upid = shift;
PVE::Ceph::Services::destroy_mgr($mgrid);
};
PVE::Ceph::Services::destroy_mgr($mgrid);
};
return $rpcenv->fork_worker('cephdestroymgr', "mgr.$mgrid", $authuser, $worker);
}});
return $rpcenv->fork_worker('cephdestroymgr', "mgr.$mgrid", $authuser, $worker);
},
});

View file

@ -23,67 +23,73 @@ use base qw(PVE::RESTHandler);
my $find_mon_ips = sub {
my ($cfg, $rados, $node, $mon_address) = @_;
my $overwrite_ips = [ PVE::Tools::split_list($mon_address) ];
my $overwrite_ips = [PVE::Tools::split_list($mon_address)];
$overwrite_ips = PVE::Network::unique_ips($overwrite_ips);
my $pubnet;
if ($rados) {
$pubnet = $rados->mon_command({ prefix => "config get" , who => "mon.",
key => "public_network", format => 'plain' });
# if not defined in the db, the result is empty, it is also always
# followed by a newline
($pubnet) = $pubnet =~ m/^(\S+)$/;
$pubnet = $rados->mon_command({
prefix => "config get",
who => "mon.",
key => "public_network",
format => 'plain',
});
# if not defined in the db, the result is empty, it is also always
# followed by a newline
($pubnet) = $pubnet =~ m/^(\S+)$/;
}
$pubnet //= $cfg->{global}->{public_network};
if (!$pubnet) {
if (scalar(@{$overwrite_ips})) {
return $overwrite_ips;
} else {
# don't refactor into '[ PVE::Cluster::remote... ]' as it uses wantarray
my $ip = PVE::Cluster::remote_node_ip($node);
return [ $ip ];
}
if (scalar(@{$overwrite_ips})) {
return $overwrite_ips;
} else {
# don't refactor into '[ PVE::Cluster::remote... ]' as it uses wantarray
my $ip = PVE::Cluster::remote_node_ip($node);
return [$ip];
}
}
my $public_nets = [ PVE::Tools::split_list($pubnet) ];
my $public_nets = [PVE::Tools::split_list($pubnet)];
if (scalar(@{$public_nets}) > 1) {
warn "Multiple Ceph public networks detected on $node: $pubnet\n";
warn "Networks must be capable of routing to each other.\n";
warn "Multiple Ceph public networks detected on $node: $pubnet\n";
warn "Networks must be capable of routing to each other.\n";
}
my $res = [];
if (!scalar(@{$overwrite_ips})) { # auto-select one address for each public network
for my $net (@{$public_nets}) {
my $allowed_ips = PVE::Network::get_local_ip_from_cidr($net);
$allowed_ips = PVE::Network::unique_ips($allowed_ips);
for my $net (@{$public_nets}) {
my $allowed_ips = PVE::Network::get_local_ip_from_cidr($net);
$allowed_ips = PVE::Network::unique_ips($allowed_ips);
die "No active IP found for the requested ceph public network '$net' on node '$node'\n"
if scalar(@$allowed_ips) < 1;
die
"No active IP found for the requested ceph public network '$net' on node '$node'\n"
if scalar(@$allowed_ips) < 1;
if (scalar(@$allowed_ips) == 1) {
push @{$res}, $allowed_ips->[0];
} else {
die "Multiple IPs for ceph public network '$net' detected on $node:\n".
join("\n", @$allowed_ips) ."\nuse 'mon-address' to specify one of them.\n";
}
}
if (scalar(@$allowed_ips) == 1) {
push @{$res}, $allowed_ips->[0];
} else {
die "Multiple IPs for ceph public network '$net' detected on $node:\n"
. join("\n", @$allowed_ips)
. "\nuse 'mon-address' to specify one of them.\n";
}
}
} else { # check if overwrite IPs are active and in any of the public networks
my $allowed_list = [];
my $allowed_list = [];
for my $net (@{$public_nets}) {
push @{$allowed_list}, @{PVE::Network::get_local_ip_from_cidr($net)};
}
for my $net (@{$public_nets}) {
push @{$allowed_list}, @{ PVE::Network::get_local_ip_from_cidr($net) };
}
my $allowed_ips = PVE::Network::unique_ips($allowed_list);
my $allowed_ips = PVE::Network::unique_ips($allowed_list);
for my $overwrite_ip (@{$overwrite_ips}) {
die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n"
if !grep { $_ eq $overwrite_ip } @{$allowed_ips};
for my $overwrite_ip (@{$overwrite_ips}) {
die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n"
if !grep { $_ eq $overwrite_ip } @{$allowed_ips};
push @{$res}, $overwrite_ip;
}
push @{$res}, $overwrite_ip;
}
}
return $res;
@ -97,17 +103,17 @@ my $ips_from_mon_host = sub {
my @hosts = PVE::Tools::split_list($mon_host);
for my $host (@hosts) {
$host =~ s|^\[?v\d+\:||; # remove beginning of vector
$host =~ s|/\d+\]?||; # remove end of vector
$host =~ s|^\[?v\d+\:||; # remove beginning of vector
$host =~ s|/\d+\]?||; # remove end of vector
($host) = PVE::Tools::parse_host_and_port($host);
next if !defined($host);
($host) = PVE::Tools::parse_host_and_port($host);
next if !defined($host);
# filter out hostnames
my $ip = PVE::JSONSchema::pve_verify_ip($host, 1);
next if !defined($ip);
# filter out hostnames
my $ip = PVE::JSONSchema::pve_verify_ip($host, 1);
next if !defined($ip);
push @{$ips}, $ip;
push @{$ips}, $ip;
}
return $ips;
@ -121,26 +127,26 @@ my $assert_mon_prerequisites = sub {
my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
for my $mon_host_ip (@{$mon_host_ips}) {
my $ip = PVE::Network::canonical_ip($mon_host_ip);
$used_ips->{$ip} = 1;
my $ip = PVE::Network::canonical_ip($mon_host_ip);
$used_ips->{$ip} = 1;
}
for my $mon (values %{$monhash}) {
next if !defined($mon->{addr});
next if !defined($mon->{addr});
for my $ip ($ips_from_mon_host->($mon->{addr})->@*) {
$ip = PVE::Network::canonical_ip($ip);
$used_ips->{$ip} = 1;
}
for my $ip ($ips_from_mon_host->($mon->{addr})->@*) {
$ip = PVE::Network::canonical_ip($ip);
$used_ips->{$ip} = 1;
}
}
for my $monip (@{$monips}) {
$monip = PVE::Network::canonical_ip($monip);
die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
$monip = PVE::Network::canonical_ip($monip);
die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
}
if (defined($monhash->{$monid})) {
die "monitor '$monid' already exists\n";
die "monitor '$monid' already exists\n";
}
};
@ -148,13 +154,15 @@ my $assert_mon_can_remove = sub {
my ($monhash, $monlist, $monid, $mondir) = @_;
if (
!defined($monhash->{$monid} ||
grep { defined($_->{name}) && $_->{name} eq $monid } $monlist->@*)
!defined(
$monhash->{$monid}
|| grep { defined($_->{name}) && $_->{name} eq $monid } $monlist->@*
)
) {
die "no such monitor id '$monid'\n"
die "no such monitor id '$monid'\n";
}
die "monitor filesystem '$mondir' does not exist on this node\n" if ! -d $mondir;
die "monitor filesystem '$mondir' does not exist on this node\n" if !-d $mondir;
die "can't remove last monitor\n" if scalar($monlist->@*) <= 1;
};
@ -178,8 +186,8 @@ my $remove_addr_from_mon_host = sub {
# ipv6 only without brackets
if ($addr =~ m/^\[?(.*?:.*?)\]?$/) {
$addr = $1;
$monhost =~ s/(^|[ ,;]+)\Q$addr\E(?:[ ,;]+|$)/$1/;
$addr = $1;
$monhost =~ s/(^|[ ,;]+)\Q$addr\E(?:[ ,;]+|$)/$1/;
}
# remove trailing separators
@ -188,7 +196,7 @@ my $remove_addr_from_mon_host = sub {
return $monhost;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'listmon',
path => '',
method => 'GET',
@ -196,73 +204,74 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
addr => { type => 'string', optional => 1 },
ceph_version => { type => 'string', optional => 1 },
ceph_version_short => { type => 'string', optional => 1 },
direxists => { type => 'string', optional => 1 },
host => { type => 'boolean', optional => 1 },
name => { type => 'string' },
quorum => { type => 'boolean', optional => 1 },
rank => { type => 'integer', optional => 1 },
service => { type => 'integer', optional => 1 },
state => { type => 'string', optional => 1 },
},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {
addr => { type => 'string', optional => 1 },
ceph_version => { type => 'string', optional => 1 },
ceph_version_short => { type => 'string', optional => 1 },
direxists => { type => 'string', optional => 1 },
host => { type => 'boolean', optional => 1 },
name => { type => 'string' },
quorum => { type => 'boolean', optional => 1 },
rank => { type => 'integer', optional => 1 },
service => { type => 'integer', optional => 1 },
state => { type => 'string', optional => 1 },
},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $res = [];
my $res = [];
my $cfg = cfs_read_file('ceph.conf');
my $cfg = cfs_read_file('ceph.conf');
my $rados = eval { PVE::RADOS->new() };
warn $@ if $@;
my $monhash = PVE::Ceph::Services::get_services_info("mon", $cfg, $rados);
my $rados = eval { PVE::RADOS->new() };
warn $@ if $@;
my $monhash = PVE::Ceph::Services::get_services_info("mon", $cfg, $rados);
if ($rados) {
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
if ($rados) {
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
my $mons = $monstat->{monmap}->{mons};
foreach my $d (@$mons) {
next if !defined($d->{name});
my $name = $d->{name};
$monhash->{$name}->{rank} = $d->{rank};
$monhash->{$name}->{addr} = $d->{addr};
if (grep { $_ eq $d->{rank} } @{$monstat->{quorum}}) {
$monhash->{$name}->{quorum} = 1;
$monhash->{$name}->{state} = 'running';
}
}
my $mons = $monstat->{monmap}->{mons};
foreach my $d (@$mons) {
next if !defined($d->{name});
my $name = $d->{name};
$monhash->{$name}->{rank} = $d->{rank};
$monhash->{$name}->{addr} = $d->{addr};
if (grep { $_ eq $d->{rank} } @{ $monstat->{quorum} }) {
$monhash->{$name}->{quorum} = 1;
$monhash->{$name}->{state} = 'running';
}
}
} else {
# we cannot check the status if we do not have a RADOS
# object, so set the state to unknown
foreach my $monid (sort keys %$monhash) {
$monhash->{$monid}->{state} = 'unknown';
}
}
} else {
# we cannot check the status if we do not have a RADOS
# object, so set the state to unknown
foreach my $monid (sort keys %$monhash) {
$monhash->{$monid}->{state} = 'unknown';
}
}
return PVE::RESTHandler::hash_to_array($monhash, 'name');
}});
return PVE::RESTHandler::hash_to_array($monhash, 'name');
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'createmon',
path => '{monid}',
method => 'POST',
@ -270,216 +279,230 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
type => 'string',
optional => 1,
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the monitor, when omitted the same as the nodename",
},
'mon-address' => {
description => 'Overwrites autodetected monitor IP address(es). ' .
'Must be in the public network(s) of Ceph.',
type => 'string', format => 'ip-list',
optional => 1,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
type => 'string',
optional => 1,
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the monitor, when omitted the same as the nodename",
},
'mon-address' => {
description => 'Overwrites autodetected monitor IP address(es). '
. 'Must be in the public network(s) of Ceph.',
type => 'string',
format => 'ip-list',
optional => 1,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_installed('ceph_mon');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::setup_pve_symlinks();
PVE::Ceph::Tools::check_ceph_installed('ceph_mon');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::setup_pve_symlinks();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $cfg = cfs_read_file('ceph.conf');
my $rados = eval { PVE::RADOS->new() }; # try a rados connection, fails for first monitor
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $cfg = cfs_read_file('ceph.conf');
my $rados = eval { PVE::RADOS->new() }; # try a rados connection, fails for first monitor
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $is_first_monitor = !(scalar(keys %$monhash) || $cfg->{global}->{mon_host});
my $is_first_monitor = !(scalar(keys %$monhash) || $cfg->{global}->{mon_host});
if (!defined($rados) && !$is_first_monitor) {
die "Could not connect to ceph cluster despite configured monitors\n";
}
if (!defined($rados) && !$is_first_monitor) {
die "Could not connect to ceph cluster despite configured monitors\n";
}
my $monid = $param->{monid} // $param->{node};
my $monsection = "mon.$monid";
my $ips = $find_mon_ips->($cfg, $rados, $param->{node}, $param->{'mon-address'});
my $monid = $param->{monid} // $param->{node};
my $monsection = "mon.$monid";
my $ips = $find_mon_ips->($cfg, $rados, $param->{node}, $param->{'mon-address'});
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
my $worker = sub {
my $upid = shift;
my $worker = sub {
my $upid = shift;
PVE::Cluster::cfs_lock_file('ceph.conf', undef, sub {
# update cfg content and reassert prereqs inside the lock
$cfg = cfs_read_file('ceph.conf');
# reopen with longer timeout
if (defined($rados)) {
$rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
}
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
PVE::Cluster::cfs_lock_file(
'ceph.conf',
undef,
sub {
# update cfg content and reassert prereqs inside the lock
$cfg = cfs_read_file('ceph.conf');
# reopen with longer timeout
if (defined($rados)) {
$rados = PVE::RADOS->new(
timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
}
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
my $client_keyring = PVE::Ceph::Tools::get_or_create_admin_keyring();
my $mon_keyring = PVE::Ceph::Tools::get_config('pve_mon_key_path');
my $client_keyring = PVE::Ceph::Tools::get_or_create_admin_keyring();
my $mon_keyring = PVE::Ceph::Tools::get_config('pve_mon_key_path');
if (! -f $mon_keyring) {
print "creating new monitor keyring\n";
run_command([
'ceph-authtool',
'--create-keyring',
$mon_keyring,
'--gen-key',
'-n',
'mon.',
'--cap',
'mon',
'allow *',
]);
run_command([
'ceph-authtool',
$mon_keyring,
'--import-keyring',
$client_keyring,
]);
}
if (!-f $mon_keyring) {
print "creating new monitor keyring\n";
run_command([
'ceph-authtool',
'--create-keyring',
$mon_keyring,
'--gen-key',
'-n',
'mon.',
'--cap',
'mon',
'allow *',
]);
run_command([
'ceph-authtool', $mon_keyring, '--import-keyring', $client_keyring,
]);
}
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
-d $mondir && die "monitor filesystem '$mondir' already exist\n";
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
-d $mondir && die "monitor filesystem '$mondir' already exist\n";
my $monmap = "/tmp/monmap";
my $monmap = "/tmp/monmap";
eval {
mkdir $mondir;
eval {
mkdir $mondir;
run_command(['chown', 'ceph:ceph', $mondir]);
run_command(['chown', 'ceph:ceph', $mondir]);
my $is_first_address = !defined($rados);
my $is_first_address = !defined($rados);
my $monaddrs = [];
my $monaddrs = [];
for my $ip (@{$ips}) {
if (Net::IP::ip_is_ipv6($ip)) {
$cfg->{global}->{ms_bind_ipv6} = 'true';
$cfg->{global}->{ms_bind_ipv4} = 'false' if $is_first_address;
} else {
$cfg->{global}->{ms_bind_ipv4} = 'true';
$cfg->{global}->{ms_bind_ipv6} = 'false' if $is_first_address;
}
for my $ip (@{$ips}) {
if (Net::IP::ip_is_ipv6($ip)) {
$cfg->{global}->{ms_bind_ipv6} = 'true';
$cfg->{global}->{ms_bind_ipv4} = 'false' if $is_first_address;
} else {
$cfg->{global}->{ms_bind_ipv4} = 'true';
$cfg->{global}->{ms_bind_ipv6} = 'false' if $is_first_address;
}
my $monaddr = Net::IP::ip_is_ipv6($ip) ? "[$ip]" : $ip;
push @{$monaddrs}, "v2:$monaddr:3300";
push @{$monaddrs}, "v1:$monaddr:6789";
my $monaddr = Net::IP::ip_is_ipv6($ip) ? "[$ip]" : $ip;
push @{$monaddrs}, "v2:$monaddr:3300";
push @{$monaddrs}, "v1:$monaddr:6789";
$is_first_address = 0;
}
$is_first_address = 0;
}
my $monmaptool_cmd = [
'monmaptool',
'--clobber',
'--addv',
$monid,
"[" . join(',', @{$monaddrs}) . "]",
'--print',
$monmap,
];
my $monmaptool_cmd = [
'monmaptool',
'--clobber',
'--addv',
$monid,
"[" . join(',', @{$monaddrs}) . "]",
'--print',
$monmap,
];
if (defined($rados)) { # we can only have a RADOS object if we have a monitor
my $mapdata = $rados->mon_command({ prefix => 'mon getmap', format => 'plain' });
file_set_contents($monmap, $mapdata);
run_command($monmaptool_cmd);
} else { # we need to create a monmap for the first monitor
push @{$monmaptool_cmd}, '--create';
run_command($monmaptool_cmd);
}
if (defined($rados)) { # we can only have a RADOS object if we have a monitor
my $mapdata =
$rados->mon_command({ prefix => 'mon getmap', format => 'plain' });
file_set_contents($monmap, $mapdata);
run_command($monmaptool_cmd);
} else { # we need to create a monmap for the first monitor
push @{$monmaptool_cmd}, '--create';
run_command($monmaptool_cmd);
}
run_command([
'ceph-mon',
'--mkfs',
'-i',
$monid,
'--monmap',
$monmap,
'--keyring',
$mon_keyring,
]);
run_command(['chown', 'ceph:ceph', '-R', $mondir]);
};
my $err = $@;
unlink $monmap;
if ($err) {
File::Path::remove_tree($mondir);
die $err;
}
run_command([
'ceph-mon',
'--mkfs',
'-i',
$monid,
'--monmap',
$monmap,
'--keyring',
$mon_keyring,
]);
run_command(['chown', 'ceph:ceph', '-R', $mondir]);
};
my $err = $@;
unlink $monmap;
if ($err) {
File::Path::remove_tree($mondir);
die $err;
}
# update ceph.conf
my $monhost = $cfg->{global}->{mon_host} // "";
# add all known monitor ips to mon_host if it does not exist
if (!defined($cfg->{global}->{mon_host})) {
for my $mon (sort keys %$monhash) {
$monhost .= " " . $monhash->{$mon}->{addr};
}
}
$monhost .= " " . join(' ', @{$ips});
$cfg->{global}->{mon_host} = $monhost;
# The IP is needed in the ceph.conf for the first boot
$cfg->{$monsection}->{public_addr} = $ips->[0];
# update ceph.conf
my $monhost = $cfg->{global}->{mon_host} // "";
# add all known monitor ips to mon_host if it does not exist
if (!defined($cfg->{global}->{mon_host})) {
for my $mon (sort keys %$monhash) {
$monhost .= " " . $monhash->{$mon}->{addr};
}
}
$monhost .= " " . join(' ', @{$ips});
$cfg->{global}->{mon_host} = $monhost;
# The IP is needed in the ceph.conf for the first boot
$cfg->{$monsection}->{public_addr} = $ips->[0];
cfs_write_file('ceph.conf', $cfg);
cfs_write_file('ceph.conf', $cfg);
PVE::Ceph::Services::ceph_service_cmd('start', $monsection);
PVE::Ceph::Services::ceph_service_cmd('start', $monsection);
if ($is_first_monitor) {
print "created the first monitor, assume it's safe to disable insecure global"
." ID reclaim for new setup\n";
eval {
run_command(
['ceph', 'config', 'set', 'mon', 'auth_allow_insecure_global_id_reclaim', 'false'],
errfunc => sub { print STDERR "$_[0]\n" },
)
};
warn "$@" if $@;
if ($is_first_monitor) {
print
"created the first monitor, assume it's safe to disable insecure global"
. " ID reclaim for new setup\n";
eval {
run_command(
[
'ceph',
'config',
'set',
'mon',
'auth_allow_insecure_global_id_reclaim',
'false',
],
errfunc => sub { print STDERR "$_[0]\n" },
);
};
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 $@;
}
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) };
warn "Enable ceph-mon\@${monid}.service failed, do manually: $@\n" if $@;
eval { PVE::Ceph::Services::ceph_service_cmd('enable', $monsection) };
warn "Enable ceph-mon\@${monid}.service failed, do manually: $@\n" if $@;
PVE::Ceph::Services::broadcast_ceph_services();
});
die $@ if $@;
# automatically create manager after the first monitor is created
if ($is_first_monitor) {
PVE::API2::Ceph::MGR->createmgr({
node => $param->{node},
id => $param->{node}
})
}
};
PVE::Ceph::Services::broadcast_ceph_services();
},
);
die $@ if $@;
# automatically create manager after the first monitor is created
if ($is_first_monitor) {
PVE::API2::Ceph::MGR->createmgr({
node => $param->{node},
id => $param->{node},
});
}
};
return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker);
}});
return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'destroymon',
path => '{monid}',
method => 'DELETE',
@ -487,122 +510,130 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
description => 'Monitor ID',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
description => 'Monitor ID',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $cfg = cfs_read_file('ceph.conf');
my $cfg = cfs_read_file('ceph.conf');
my $monid = $param->{monid};
my $monsection = "mon.$monid";
my $monid = $param->{monid};
my $monsection = "mon.$monid";
my $rados = PVE::RADOS->new();
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
my $monlist = $monstat->{monmap}->{mons};
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $rados = PVE::RADOS->new();
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
my $monlist = $monstat->{monmap}->{mons};
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
my $worker = sub {
my $upid = shift;
PVE::Cluster::cfs_lock_file('ceph.conf', undef, sub {
# reload info and recheck
$cfg = cfs_read_file('ceph.conf');
my $worker = sub {
my $upid = shift;
PVE::Cluster::cfs_lock_file(
'ceph.conf',
undef,
sub {
# reload info and recheck
$cfg = cfs_read_file('ceph.conf');
# reopen with longer timeout
$rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$monstat = $rados->mon_command({ prefix => 'quorum_status' });
$monlist = $monstat->{monmap}->{mons};
# reopen with longer timeout
$rados = PVE::RADOS->new(
timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$monstat = $rados->mon_command({ prefix => 'quorum_status' });
$monlist = $monstat->{monmap}->{mons};
my $addrs = [];
my $addrs = [];
my $add_addr = sub {
my ($addr) = @_;
my $add_addr = sub {
my ($addr) = @_;
# extract the ip without port and nonce (if present)
($addr) = $addr =~ m|^(.*):\d+(/\d+)?$|;
($addr) = $addr =~ m|^\[?(.*?)\]?$|; # remove brackets
push @{$addrs}, $addr;
};
# extract the ip without port and nonce (if present)
($addr) = $addr =~ m|^(.*):\d+(/\d+)?$|;
($addr) = $addr =~ m|^\[?(.*?)\]?$|; # remove brackets
push @{$addrs}, $addr;
};
for my $mon (@$monlist) {
if ($mon->{name} eq $monid) {
if ($mon->{public_addrs} && $mon->{public_addrs}->{addrvec}) {
my $addrvec = $mon->{public_addrs}->{addrvec};
for my $addr (@{$addrvec}) {
$add_addr->($addr->{addr});
}
} else {
$add_addr->($mon->{public_addr} // $mon->{addr});
}
last;
}
}
for my $mon (@$monlist) {
if ($mon->{name} eq $monid) {
if ($mon->{public_addrs} && $mon->{public_addrs}->{addrvec}) {
my $addrvec = $mon->{public_addrs}->{addrvec};
for my $addr (@{$addrvec}) {
$add_addr->($addr->{addr});
}
} else {
$add_addr->($mon->{public_addr} // $mon->{addr});
}
last;
}
}
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
# this also stops the service
$rados->mon_command({ prefix => "mon remove", name => $monid, format => 'plain' });
# this also stops the service
$rados->mon_command(
{ prefix => "mon remove", name => $monid, format => 'plain' });
# delete section
delete $cfg->{$monsection};
# delete section
delete $cfg->{$monsection};
# delete from mon_host
if (my $monhost = $cfg->{global}->{mon_host}) {
my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
# delete from mon_host
if (my $monhost = $cfg->{global}->{mon_host}) {
my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
for my $addr (@{$addrs}) {
$monhost = $remove_addr_from_mon_host->($monhost, $addr);
for my $addr (@{$addrs}) {
$monhost = $remove_addr_from_mon_host->($monhost, $addr);
# also remove matching IPs that differ syntactically
if (PVE::JSONSchema::pve_verify_ip($addr, 1)) {
$addr = PVE::Network::canonical_ip($addr);
# also remove matching IPs that differ syntactically
if (PVE::JSONSchema::pve_verify_ip($addr, 1)) {
$addr = PVE::Network::canonical_ip($addr);
for my $mon_host_ip (@{$mon_host_ips}) {
# match canonical addresses, but remove as present in mon_host
if (PVE::Network::canonical_ip($mon_host_ip) eq $addr) {
$monhost = $remove_addr_from_mon_host->($monhost, $mon_host_ip);
}
}
}
}
$cfg->{global}->{mon_host} = $monhost;
}
for my $mon_host_ip (@{$mon_host_ips}) {
# match canonical addresses, but remove as present in mon_host
if (PVE::Network::canonical_ip($mon_host_ip) eq $addr) {
$monhost =
$remove_addr_from_mon_host->($monhost, $mon_host_ip);
}
}
}
}
$cfg->{global}->{mon_host} = $monhost;
}
cfs_write_file('ceph.conf', $cfg);
File::Path::remove_tree($mondir);
eval { PVE::Ceph::Services::ceph_service_cmd('disable', $monsection) };
warn $@ if $@;
PVE::Ceph::Services::broadcast_ceph_services();
});
cfs_write_file('ceph.conf', $cfg);
File::Path::remove_tree($mondir);
eval { PVE::Ceph::Services::ceph_service_cmd('disable', $monsection) };
warn $@ if $@;
PVE::Ceph::Services::broadcast_ceph_services();
},
);
die $@ if $@;
};
die $@ if $@;
};
return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker);
}});
return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker);
},
});
1;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -5,51 +5,48 @@ use warnings;
use PVE::API2::ACME;
use PVE::Certificate;
use PVE::CertHelpers;;
use PVE::CertHelpers;
use PVE::Exception qw(raise_param_exc);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Tools qw(extract_param file_get_contents file_set_contents);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::ACME",
path => 'acme',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => { user => 'all' },
description => "Node index.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return [
{ name => 'acme' },
{ name => 'custom' },
{ name => 'info' },
];
return [
{ name => 'acme' }, { name => 'custom' }, { name => 'info' },
];
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'info',
path => 'info',
method => 'GET',
@ -57,157 +54,159 @@ __PACKAGE__->register_method ({
proxyto => 'node',
description => "Get information about node's certificates.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => get_standard_option('pve-certificate-info'),
type => 'array',
items => get_standard_option('pve-certificate-info'),
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node_path = "/etc/pve/nodes/$param->{node}";
my $node_path = "/etc/pve/nodes/$param->{node}";
my $res = [];
my $cert_paths = [
'/etc/pve/pve-root-ca.pem',
"$node_path/pve-ssl.pem",
"$node_path/pveproxy-ssl.pem",
];
for my $path (@$cert_paths) {
eval {
my $info = PVE::Certificate::get_certificate_info($path);
push @$res, $info if $info;
};
}
return $res;
my $res = [];
my $cert_paths = [
'/etc/pve/pve-root-ca.pem', "$node_path/pve-ssl.pem", "$node_path/pveproxy-ssl.pem",
];
for my $path (@$cert_paths) {
eval {
my $info = PVE::Certificate::get_certificate_info($path);
push @$res, $info if $info;
};
}
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'upload_custom_cert',
path => 'custom',
method => 'POST',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => 'Upload or update custom certificate chain and key.',
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
certificates => {
type => 'string',
format => 'pem-certificate-chain',
description => 'PEM encoded certificate (chain).',
},
key => {
type => 'string',
description => 'PEM encoded private key.',
format => 'pem-string',
optional => 1,
},
force => {
type => 'boolean',
description => 'Overwrite existing custom or ACME certificate files.',
optional => 1,
default => 0,
},
restart => {
type => 'boolean',
description => 'Restart pveproxy.',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
certificates => {
type => 'string',
format => 'pem-certificate-chain',
description => 'PEM encoded certificate (chain).',
},
key => {
type => 'string',
description => 'PEM encoded private key.',
format => 'pem-string',
optional => 1,
},
force => {
type => 'boolean',
description => 'Overwrite existing custom or ACME certificate files.',
optional => 1,
default => 0,
},
restart => {
type => 'boolean',
description => 'Restart pveproxy.',
optional => 1,
default => 0,
},
},
},
returns => get_standard_option('pve-certificate-info'),
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $certs = extract_param($param, 'certificates');
$certs = PVE::Certificate::strip_leading_text($certs);
my $certs = extract_param($param, 'certificates');
$certs = PVE::Certificate::strip_leading_text($certs);
my $key = extract_param($param, 'key');
if ($key) {
$key = PVE::Certificate::strip_leading_text($key);
} else {
raise_param_exc({'key' => "Attempted to upload custom certificate without (existing) key."})
if ! -e "${cert_prefix}.key";
}
my $key = extract_param($param, 'key');
if ($key) {
$key = PVE::Certificate::strip_leading_text($key);
} else {
raise_param_exc(
{ 'key' => "Attempted to upload custom certificate without (existing) key." })
if !-e "${cert_prefix}.key";
}
my $info;
my $info;
my $code = sub {
print "Setting custom certificate files\n";
$info = PVE::CertHelpers::set_cert_files($certs, $key, $cert_prefix, $param->{force});
my $code = sub {
print "Setting custom certificate files\n";
$info =
PVE::CertHelpers::set_cert_files($certs, $key, $cert_prefix, $param->{force});
if ($param->{restart}) {
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
}
};
if ($param->{restart}) {
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
}
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
return $info;
}});
return $info;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'remove_custom_cert',
path => 'custom',
method => 'DELETE',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => 'DELETE custom certificate chain and key.',
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
restart => {
type => 'boolean',
description => 'Restart pveproxy.',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
restart => {
type => 'boolean',
description => 'Restart pveproxy.',
optional => 1,
default => 0,
},
},
},
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $node = extract_param($param, 'node');
my $cert_prefix = PVE::CertHelpers::cert_path_prefix($node);
my $code = sub {
print "Deleting custom certificate files\n";
unlink "${cert_prefix}.pem";
unlink "${cert_prefix}.key";
my $code = sub {
print "Deleting custom certificate files\n";
unlink "${cert_prefix}.pem";
unlink "${cert_prefix}.key";
if ($param->{restart}) {
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
}
};
if ($param->{restart}) {
print "Restarting pveproxy\n";
PVE::Tools::run_command(['systemctl', 'reload-or-restart', 'pveproxy']);
}
};
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
PVE::CertHelpers::cert_lock(10, $code);
die "$@\n" if $@;
return undef;
}});
return undef;
},
});
1;

File diff suppressed because it is too large Load diff

View file

@ -25,8 +25,8 @@ sub get_included_vmids {
my $all_vmids = {};
for my $job ($legacy_jobs->@*, grep { $_->{type} eq 'vzdump' } values $jobs->{ids}->%*) {
my $job_included_guests = PVE::VZDump::get_included_guests($job);
$all_vmids->{$_} = 1 for map { $_->@* } values %{$job_included_guests};
my $job_included_guests = PVE::VZDump::get_included_guests($job);
$all_vmids->{$_} = 1 for map { $_->@* } values %{$job_included_guests};
}
return $all_vmids;
@ -38,28 +38,29 @@ __PACKAGE__->register_method({
method => 'GET',
description => "Index for backup info related endpoints",
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
description => 'Directory index.',
items => {
type => "object",
properties => {
subdir => {
type => 'string',
description => 'API sub-directory endpoint',
},
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
type => 'array',
description => 'Directory index.',
items => {
type => "object",
properties => {
subdir => {
type => 'string',
description => 'API sub-directory endpoint',
},
},
},
links => [{ rel => 'child', href => "{subdir}" }],
},
code => sub {
return [
{ subdir => 'not-backed-up' },
];
}});
return [
{ subdir => 'not-backed-up' },
];
},
});
__PACKAGE__->register_method({
name => 'get_guests_not_in_backup',
@ -68,72 +69,75 @@ __PACKAGE__->register_method({
protected => 1,
description => "Shows all guests which are not covered by any backup job.",
permissions => {
check => ['perm', '/', ['Sys.Audit']],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
description => 'Contains the guest objects.',
items => {
type => 'object',
properties => {
vmid => {
type => 'integer',
description => 'VMID of the guest.',
},
name => {
type => 'string',
description => 'Name of the guest',
optional => 1,
},
type => {
type => 'string',
description => 'Type of the guest.',
enum => ['qemu', 'lxc'],
},
},
},
type => 'array',
description => 'Contains the guest objects.',
items => {
type => 'object',
properties => {
vmid => {
type => 'integer',
description => 'VMID of the guest.',
},
name => {
type => 'string',
description => 'Name of the guest',
optional => 1,
},
type => {
type => 'string',
description => 'Type of the guest.',
enum => ['qemu', 'lxc'],
},
},
},
},
code => sub {
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $included_vmids = get_included_vmids();
my $vmlist = PVE::Cluster::get_vmlist();
my $included_vmids = get_included_vmids();
my $vmlist = PVE::Cluster::get_vmlist();
# remove VMIDs to which the user has no permission to not leak infos like the guest name
my @allowed_vmids = grep { $rpcenv->check($user, "/vms/$_", [ 'VM.Audit' ], 1) } keys $vmlist->{ids}->%*;
# remove VMIDs to which the user has no permission to not leak infos like the guest name
my @allowed_vmids =
grep { $rpcenv->check($user, "/vms/$_", ['VM.Audit'], 1) } keys $vmlist->{ids}->%*;
my $result = [];
for my $vmid (@allowed_vmids) {
next if $included_vmids->{$vmid};
my $result = [];
for my $vmid (@allowed_vmids) {
next if $included_vmids->{$vmid};
my ($type, $node) = $vmlist->{ids}->{$vmid}->@{'type', 'node'};
my ($type, $node) = $vmlist->{ids}->{$vmid}->@{ 'type', 'node' };
my ($conf, $name);
if ($type eq 'qemu') {
$conf = PVE::QemuConfig->load_config($vmid, $node);
$name = $conf->{name};
} elsif ($type eq 'lxc') {
$conf = PVE::LXC::Config->load_config($vmid, $node);
$name = $conf->{hostname};
} else {
die "Unexpected error: unknown guest type for VMID $vmid, neither QEMU nor LXC\n";
}
my ($conf, $name);
if ($type eq 'qemu') {
$conf = PVE::QemuConfig->load_config($vmid, $node);
$name = $conf->{name};
} elsif ($type eq 'lxc') {
$conf = PVE::LXC::Config->load_config($vmid, $node);
$name = $conf->{hostname};
} else {
die
"Unexpected error: unknown guest type for VMID $vmid, neither QEMU nor LXC\n";
}
my $entry = {
vmid => int($vmid),
type => $type,
};
$entry->{name} = $name if defined($name);
my $entry = {
vmid => int($vmid),
type => $type,
};
$entry->{name} = $name if defined($name);
push @{$result}, $entry;
}
push @{$result}, $entry;
}
return $result;
}});
return $result;
},
});
1;

View file

@ -0,0 +1,139 @@
package PVE::API2::Cluster::BulkActions;
use warnings;
use strict;
use Storable qw(dclone);
use JSON;
use PVE::Exception qw(raise_param_exc);
use PVE::Tools qw(extract_param);
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => 'Index for cluster-wide bulk-action API endpoints.',
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => 'object',
properties => {},
},
links => [ { rel => 'child', href => '{name}' } ],
},
code => sub {
my $result = [
{ name => 'migrate' },
{ name => 'start' },
{ name => 'stop' },
];
return $result;
}
});
my $guest_format = {
vmid => {
defau
},
};
__PACKAGE__->register_method ({
name => 'migrate',
path => 'migrate',
method => 'POST',
description => 'Returns a list of all entities that can be used as notification targets' .
' (endpoints and groups).',
permissions => {
description => "The 'VM.Migrate' permission is required on '/' or on '/vms/<ID>' for each "
."ID passed via the 'vms' parameter.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
'guests' => {
type => 'array',
description => '',
items => {
type => 'string',
format => $guest_format,
}
},
},
},
returns => {
type => 'string',
items => {
type => 'object',
properties => {
name => {
description => 'Name of the endpoint/group.',
type => 'string',
format => 'pve-configid',
},
'type' => {
description => 'Type of the endpoint or group.',
type => 'string',
enum => [qw(sendmail gotify group)],
},
'comment' => {
description => 'Comment',
type => 'string',
optional => 1,
},
},
},
links => [ { rel => 'child', href => '{name}' } ],
},
code => sub {
my $config = PVE::Notify::read_config();
my $rpcenv = PVE::RPCEnvironment::get();
my $targets = eval {
my $result = [];
for my $target (@{$config->get_sendmail_endpoints()}) {
push @$result, {
name => $target->{name},
comment => $target->{comment},
type => 'sendmail',
};
}
for my $target (@{$config->get_gotify_endpoints()}) {
push @$result, {
name => $target->{name},
comment => $target->{comment},
type => 'gotify',
};
}
for my $target (@{$config->get_groups()}) {
push @$result, {
name => $target->{name},
comment => $target->{comment},
type => 'group',
};
}
$result
};
raise_api_error($@) if $@;
return filter_entities_by_privs($rpcenv, $targets);
}
});
1;

View file

@ -17,296 +17,297 @@ use PVE::Tools qw(extract_param);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'cephindex',
path => '',
method => 'GET',
description => "Cluster ceph index.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'metadata' },
{ name => 'status' },
{ name => 'flags' },
];
my $result = [
{ name => 'metadata' }, { name => 'status' }, { name => 'flags' },
];
return $result;
}
return $result;
},
});
my $metadata_common_props = {
hostname => {
type => "string",
description => "Hostname on which the service is running.",
type => "string",
description => "Hostname on which the service is running.",
},
ceph_release => {
type => "string",
description => "Ceph release codename currently used.",
type => "string",
description => "Ceph release codename currently used.",
},
ceph_version => {
type => "string",
description => "Version info currently used by the service.",
type => "string",
description => "Version info currently used by the service.",
},
ceph_version_short => {
type => "string",
description => "Short version (numerical) info currently used by the service.",
type => "string",
description => "Short version (numerical) info currently used by the service.",
},
mem_total_kb => {
type => "integer",
description => "Memory consumption of the service.",
type => "integer",
description => "Memory consumption of the service.",
},
mem_swap_kb => {
type => "integer",
description => "Memory of the service currently in swap.",
type => "integer",
description => "Memory of the service currently in swap.",
},
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'metadata',
path => 'metadata',
method => 'GET',
description => "Get ceph metadata.",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
scope => {
type => 'string',
optional => 1,
default => 'all',
enum => ['all', 'versions', ],
},
},
additionalProperties => 0,
properties => {
scope => {
type => 'string',
optional => 1,
default => 'all',
enum => ['all', 'versions'],
},
},
},
returns => {
type => 'object',
description => "Items for each type of service containing objects for each instance.",
properties => {
mds => {
type => "object",
description => "Metadata servers configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addr => {
type => "string",
description => "Bind addresses and ports.",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
mgr => {
type => "object",
description => "Managers configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addr => {
type => "string",
description => "Bind address",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
mon => {
type => "object",
description => "Monitors configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addrs => {
type => "string",
description => "Bind addresses and ports.",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
node => {
type => "object",
description => "Ceph version installed on the nodes.",
properties => {
"{node}" => {
type => "object",
properties => {
buildcommit => {
type => "string",
description => "GIT commit used for the build.",
},
version => {
type => "object",
description => "Version info.",
properties => {
str => {
type => "string",
description => "Version as single string.",
},
parts => {
type => "array",
description => "major, minor & patch",
},
},
},
},
},
},
},
osd => {
type => "array",
description => "OSDs configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
id => {
type => "integer",
description => "OSD ID.",
},
front_addr => {
type => "string",
description => "Bind addresses and ports for frontend traffic to OSDs.",
},
back_addr => {
type => "string",
description => "Bind addresses and ports for backend inter OSD traffic.",
},
device_id => {
type => "string",
description => "Devices used by the OSD.",
},
osd_data => {
type => "string",
description => "Path to the OSD data directory.",
},
osd_objectstore => {
type => "string",
description => "OSD objectstore type.",
},
%{$metadata_common_props},
},
},
},
},
},
type => 'object',
description => "Items for each type of service containing objects for each instance.",
properties => {
mds => {
type => "object",
description =>
"Metadata servers configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addr => {
type => "string",
description => "Bind addresses and ports.",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
mgr => {
type => "object",
description => "Managers configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addr => {
type => "string",
description => "Bind address",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
mon => {
type => "object",
description => "Monitors configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
addrs => {
type => "string",
description => "Bind addresses and ports.",
},
name => {
type => "string",
description => "Name of the service instance.",
},
%{$metadata_common_props},
},
},
},
},
node => {
type => "object",
description => "Ceph version installed on the nodes.",
properties => {
"{node}" => {
type => "object",
properties => {
buildcommit => {
type => "string",
description => "GIT commit used for the build.",
},
version => {
type => "object",
description => "Version info.",
properties => {
str => {
type => "string",
description => "Version as single string.",
},
parts => {
type => "array",
description => "major, minor & patch",
},
},
},
},
},
},
},
osd => {
type => "array",
description => "OSDs configured in the cluster and their properties.",
properties => {
"{id}" => {
type => "object",
description => "Useful properties are listed, but not the full list.",
properties => {
id => {
type => "integer",
description => "OSD ID.",
},
front_addr => {
type => "string",
description =>
"Bind addresses and ports for frontend traffic to OSDs.",
},
back_addr => {
type => "string",
description =>
"Bind addresses and ports for backend inter OSD traffic.",
},
device_id => {
type => "string",
description => "Devices used by the OSD.",
},
osd_data => {
type => "string",
description => "Path to the OSD data directory.",
},
osd_objectstore => {
type => "string",
description => "OSD objectstore type.",
},
%{$metadata_common_props},
},
},
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $scope = $param->{scope} // 'all';
my $scope = $param->{scope} // 'all';
my $res = {};
my $res = {};
if (defined(my $versions = PVE::Ceph::Services::get_ceph_versions())) {
$res->{node} = $versions;
}
if (defined(my $versions = PVE::Ceph::Services::get_ceph_versions())) {
$res->{node} = $versions;
}
return $res if ($scope eq 'versions');
return $res if ($scope eq 'versions');
# only check now, we want to allow calls with scope 'versions' on non-ceph nodes too!
PVE::Ceph::Tools::check_ceph_inited();
my $rados = PVE::RADOS->new();
# only check now, we want to allow calls with scope 'versions' on non-ceph nodes too!
PVE::Ceph::Tools::check_ceph_inited();
my $rados = PVE::RADOS->new();
for my $type ( qw(mon mgr mds) ) {
my $typedata = PVE::Ceph::Services::get_cluster_service($type);
my $data = {};
for my $host (sort keys %$typedata) {
for my $service (sort keys %{$typedata->{$host}}) {
$data->{"$service\@$host"} = $typedata->{$host}->{$service};
}
}
for my $type (qw(mon mgr mds)) {
my $typedata = PVE::Ceph::Services::get_cluster_service($type);
my $data = {};
for my $host (sort keys %$typedata) {
for my $service (sort keys %{ $typedata->{$host} }) {
$data->{"$service\@$host"} = $typedata->{$host}->{$service};
}
}
# get data from metadata call and merge 'our' data
my $services = $rados->mon_command({ prefix => "$type metadata" });
for my $service ( @$services ) {
my $hostname = $service->{hostname};
next if !defined($hostname); # can happen if node is dead
# get data from metadata call and merge 'our' data
my $services = $rados->mon_command({ prefix => "$type metadata" });
for my $service (@$services) {
my $hostname = $service->{hostname};
next if !defined($hostname); # can happen if node is dead
my $servicename = $service->{name} // $service->{id};
my $id = "$servicename\@$hostname";
my $servicename = $service->{name} // $service->{id};
my $id = "$servicename\@$hostname";
if ($data->{$id}) { # copy values over to the metadata hash
for my $k (keys %{$data->{$id}}) {
$service->{$k} = $data->{$id}->{$k};
}
}
$data->{$id} = $service;
}
if ($data->{$id}) { # copy values over to the metadata hash
for my $k (keys %{ $data->{$id} }) {
$service->{$k} = $data->{$id}->{$k};
}
}
$data->{$id} = $service;
}
$res->{$type} = $data;
}
$res->{$type} = $data;
}
$res->{osd} = $rados->mon_command({ prefix => "osd metadata" });
$res->{osd} = $rados->mon_command({ prefix => "osd metadata" });
return $res;
}
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'status',
path => 'status',
method => 'GET',
description => "Get ceph status.",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Datastore.Audit'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => { },
additionalProperties => 0,
properties => {},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
return PVE::Ceph::Tools::ceph_cluster_status();
}
return PVE::Ceph::Tools::ceph_cluster_status();
},
});
my $possible_flags = PVE::Ceph::Tools::get_possible_osd_flags();
my $possible_flags_list = [ sort keys %$possible_flags ];
my $possible_flags_list = [sort keys %$possible_flags];
my $get_current_set_flags = sub {
my $rados = shift;
@ -318,198 +319,200 @@ my $get_current_set_flags = sub {
return { map { $_ => 1 } PVE::Tools::split_list($setflags) };
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_all_flags',
path => 'flags',
method => 'GET',
description => "get the status of all ceph flags",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
additionalProperties => 1,
properties => {
name => {
description => "Flag name.",
type => 'string', enum => $possible_flags_list,
},
description => {
description => "Flag description.",
type => 'string',
},
value => {
description => "Flag value.",
type => 'boolean',
},
},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
additionalProperties => 1,
properties => {
name => {
description => "Flag name.",
type => 'string',
enum => $possible_flags_list,
},
description => {
description => "Flag description.",
type => 'string',
},
value => {
description => "Flag value.",
type => 'boolean',
},
},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_configured();
PVE::Ceph::Tools::check_ceph_configured();
my $setflags = $get_current_set_flags->();
my $setflags = $get_current_set_flags->();
my $res = [];
foreach my $flag (@$possible_flags_list) {
my $el = {
name => $flag,
description => $possible_flags->{$flag}->{description},
value => 0,
};
my $res = [];
foreach my $flag (@$possible_flags_list) {
my $el = {
name => $flag,
description => $possible_flags->{$flag}->{description},
value => 0,
};
my $realflag = PVE::Ceph::Tools::get_real_flag_name($flag);
if ($setflags->{$realflag}) {
$el->{value} = 1;
}
my $realflag = PVE::Ceph::Tools::get_real_flag_name($flag);
if ($setflags->{$realflag}) {
$el->{value} = 1;
}
push @$res, $el;
}
push @$res, $el;
}
return $res;
}
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'set_flags',
path => 'flags',
method => 'PUT',
description => "Set/Unset multiple ceph flags at once.",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => $possible_flags,
additionalProperties => 0,
properties => $possible_flags,
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_configured();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_configured();
my $worker = sub {
my $rados = PVE::RADOS->new(); # (re-)open for forked worker
my $worker = sub {
my $rados = PVE::RADOS->new(); # (re-)open for forked worker
my $setflags = $get_current_set_flags->($rados);
my $setflags = $get_current_set_flags->($rados);
my $errors = 0;
foreach my $flag (@$possible_flags_list) {
next if !defined($param->{$flag});
my $val = $param->{$flag};
my $realflag = PVE::Ceph::Tools::get_real_flag_name($flag);
my $errors = 0;
foreach my $flag (@$possible_flags_list) {
next if !defined($param->{$flag});
my $val = $param->{$flag};
my $realflag = PVE::Ceph::Tools::get_real_flag_name($flag);
next if !$val == !$setflags->{$realflag}; # we do not set/unset flags to the same state
next if !$val == !$setflags->{$realflag}; # we do not set/unset flags to the same state
my $prefix = $val ? 'set' : 'unset';
eval {
print "$prefix $flag\n";
$rados->mon_command({ prefix => "osd $prefix", key => $flag, });
};
if (my $err = $@) {
warn "error with $flag: '$err'\n";
$errors++;
}
}
my $prefix = $val ? 'set' : 'unset';
eval {
print "$prefix $flag\n";
$rados->mon_command({ prefix => "osd $prefix", key => $flag });
};
if (my $err = $@) {
warn "error with $flag: '$err'\n";
$errors++;
}
}
if ($errors) {
die "could not set/unset $errors flags\n";
}
};
if ($errors) {
die "could not set/unset $errors flags\n";
}
};
return $rpcenv->fork_worker('cephsetflags', undef, $user, $worker);
}});
return $rpcenv->fork_worker('cephsetflags', undef, $user, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get_flag',
path => 'flags/{flag}',
method => 'GET',
description => "Get the status of a specific ceph flag.",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
flag => {
description => "The name of the flag name to get.",
type => 'string', enum => $possible_flags_list,
},
},
additionalProperties => 0,
properties => {
flag => {
description => "The name of the flag name to get.",
type => 'string',
enum => $possible_flags_list,
},
},
},
returns => {
type => 'boolean',
type => 'boolean',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_configured();
PVE::Ceph::Tools::check_ceph_configured();
my $realflag = PVE::Ceph::Tools::get_real_flag_name($param->{flag});
my $realflag = PVE::Ceph::Tools::get_real_flag_name($param->{flag});
my $setflags = $get_current_set_flags->();
if ($setflags->{$realflag}) {
return 1;
}
my $setflags = $get_current_set_flags->();
if ($setflags->{$realflag}) {
return 1;
}
return 0;
}});
return 0;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update_flag',
path => 'flags/{flag}',
method => 'PUT',
description => "Set or clear (unset) a specific ceph flag",
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
flag => {
description => 'The ceph flag to update',
type => 'string',
enum => $possible_flags_list,
},
value => {
description => 'The new value of the flag',
type => 'boolean',
},
},
additionalProperties => 0,
properties => {
flag => {
description => 'The ceph flag to update',
type => 'string',
enum => $possible_flags_list,
},
value => {
description => 'The new value of the flag',
type => 'boolean',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_configured();
PVE::Ceph::Tools::check_ceph_configured();
my $cmd = $param->{value} ? 'set' : 'unset';
my $cmd = $param->{value} ? 'set' : 'unset';
my $rados = PVE::RADOS->new();
$rados->mon_command({
prefix => "osd $cmd",
key => $param->{flag},
});
return undef;
}});
my $rados = PVE::RADOS->new();
$rados->mon_command({
prefix => "osd $cmd",
key => $param->{flag},
});
return undef;
},
});
1;

View file

@ -10,7 +10,7 @@ use PVE::API2::Jobs::RealmSync;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Jobs::RealmSync",
path => 'realm-sync',
});
@ -22,29 +22,29 @@ __PACKAGE__->register_method({
description => "Index for jobs related endpoints.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
description => 'Directory index.',
items => {
type => "object",
properties => {
subdir => {
type => 'string',
description => 'API sub-directory endpoint',
},
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
type => 'array',
description => 'Directory index.',
items => {
type => "object",
properties => {
subdir => {
type => 'string',
description => 'API sub-directory endpoint',
},
},
},
links => [{ rel => 'child', href => "{subdir}" }],
},
code => sub {
return [
{ subdir => 'schedule-analyze' },
{ subdir => 'realm-sync' },
];
}});
return [
{ subdir => 'schedule-analyze' }, { subdir => 'realm-sync' },
];
},
});
__PACKAGE__->register_method({
name => 'schedule-analyze',
@ -53,67 +53,72 @@ __PACKAGE__->register_method({
description => "Returns a list of future schedule runtimes.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
schedule => {
description => "Job schedule. The format is a subset of `systemd` calendar events.",
type => 'string', format => 'pve-calendar-event',
maxLength => 128,
},
starttime => {
description => "UNIX timestamp to start the calculation from. Defaults to the current time.",
optional => 1,
type => 'integer',
},
iterations => {
description => "Number of event-iteration to simulate and return.",
optional => 1,
type => 'integer',
minimum => 1,
maximum => 100,
default => 10,
},
},
additionalProperties => 0,
properties => {
schedule => {
description =>
"Job schedule. The format is a subset of `systemd` calendar events.",
type => 'string',
format => 'pve-calendar-event',
maxLength => 128,
},
starttime => {
description =>
"UNIX timestamp to start the calculation from. Defaults to the current time.",
optional => 1,
type => 'integer',
},
iterations => {
description => "Number of event-iteration to simulate and return.",
optional => 1,
type => 'integer',
minimum => 1,
maximum => 100,
default => 10,
},
},
},
returns => {
type => 'array',
description => 'An array of the next <iterations> events since <starttime>.',
items => {
type => 'object',
properties => {
timestamp => {
type => 'integer',
description => 'UNIX timestamp for the run.',
},
utc => {
type => 'string',
description => "UTC timestamp for the run.",
},
},
},
type => 'array',
description => 'An array of the next <iterations> events since <starttime>.',
items => {
type => 'object',
properties => {
timestamp => {
type => 'integer',
description => 'UNIX timestamp for the run.',
},
utc => {
type => 'string',
description => "UTC timestamp for the run.",
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $starttime = $param->{starttime} // time();
my $iterations = $param->{iterations} // 10;
my $schedule = $param->{schedule};
my $starttime = $param->{starttime} // time();
my $iterations = $param->{iterations} // 10;
my $schedule = $param->{schedule};
my $result = [];
my $result = [];
my $event = PVE::CalendarEvent::parse_calendar_event($schedule);
my $event = PVE::CalendarEvent::parse_calendar_event($schedule);
for (my $count = 0; $count < $iterations; $count++) {
my $next = PVE::CalendarEvent::compute_next_event($event, $starttime);
last if !defined($next);
push @$result, {
timestamp => $next,
utc => scalar(gmtime($next)),
};
$starttime = $next;
}
for (my $count = 0; $count < $iterations; $count++) {
my $next = PVE::CalendarEvent::compute_next_event($event, $starttime);
last if !defined($next);
push @$result,
{
timestamp => $next,
utc => scalar(gmtime($next)),
};
$starttime = $next;
}
return $result;
}});
return $result;
},
});
1;

View file

@ -9,50 +9,49 @@ use PVE::API2::Cluster::Mapping::USB;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Cluster::Mapping::Dir",
path => 'dir',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Cluster::Mapping::PCI",
path => 'pci',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Cluster::Mapping::USB",
path => 'usb',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "List resource types.",
permissions => {
user => 'all',
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'dir' },
{ name => 'pci' },
{ name => 'usb' },
];
my $result = [
{ name => 'dir' }, { name => 'pci' }, { name => 'usb' },
];
return $result;
}});
return $result;
},
});
1;

View file

@ -14,295 +14,314 @@ use PVE::Tools qw(extract_param);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
# only proxy if we give the 'check-node' parameter
proxyto_callback => sub {
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
},
description => "List directory mapping",
permissions => {
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
." 'Mapping.Audit' permissions on '/mapping/dir/<id>'.",
user => 'all',
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
. " 'Mapping.Audit' permissions on '/mapping/dir/<id>'.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
'check-node' => get_standard_option('pve-node', {
description => "If given, checks the configurations on the given node for"
." correctness, and adds relevant diagnostics for the directory to the response.",
optional => 1,
}),
},
additionalProperties => 0,
properties => {
'check-node' => get_standard_option(
'pve-node',
{
description => "If given, checks the configurations on the given node for"
. " correctness, and adds relevant diagnostics for the directory to the response.",
optional => 1,
},
),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping."
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
checks => {
type => "array",
optional => 1,
description => "A list of checks, only present if 'check-node' is set.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
enum => ['warning', 'error'],
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
}
},
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping.",
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
checks => {
type => "array",
optional => 1,
description => "A list of checks, only present if 'check-node' is set.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
enum => ['warning', 'error'],
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
},
},
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $check_node = $param->{'check-node'};
my $local_node = PVE::INotify::nodename();
my $check_node = $param->{'check-node'};
my $local_node = PVE::INotify::nodename();
die "wrong node to check - $check_node != $local_node\n"
if defined($check_node) && $check_node ne 'localhost' && $check_node ne $local_node;
die "wrong node to check - $check_node != $local_node\n"
if defined($check_node) && $check_node ne 'localhost' && $check_node ne $local_node;
my $cfg = PVE::Mapping::Dir::config();
my $cfg = PVE::Mapping::Dir::config();
my $can_see_mapping_privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
my $can_see_mapping_privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
my $res = [];
for my $id (keys $cfg->{ids}->%*) {
next if !$rpcenv->check_any($authuser, "/mapping/dir/$id", $can_see_mapping_privs, 1);
next if !$cfg->{ids}->{$id};
my $res = [];
for my $id (keys $cfg->{ids}->%*) {
next
if !$rpcenv->check_any($authuser, "/mapping/dir/$id", $can_see_mapping_privs, 1);
next if !$cfg->{ids}->{$id};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
if (defined($check_node)) {
$entry->{checks} = [];
if (my $mappings = PVE::Mapping::Dir::get_node_mapping($cfg, $id, $check_node)) {
if (!scalar($mappings->@*)) {
push $entry->{checks}->@*, {
severity => 'warning',
message => "No mapping for node $check_node.",
};
}
for my $mapping ($mappings->@*) {
eval { PVE::Mapping::Dir::assert_valid($mapping) };
if (my $err = $@) {
push $entry->{checks}->@*, {
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
if (defined($check_node)) {
$entry->{checks} = [];
if (
my $mappings = PVE::Mapping::Dir::get_node_mapping($cfg, $id, $check_node)
) {
if (!scalar($mappings->@*)) {
push $entry->{checks}->@*,
{
severity => 'warning',
message => "No mapping for node $check_node.",
};
}
for my $mapping ($mappings->@*) {
eval { PVE::Mapping::Dir::assert_valid($mapping) };
if (my $err = $@) {
push $entry->{checks}->@*,
{
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
push @$res, $entry;
}
push @$res, $entry;
}
return $res;
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get',
protected => 1,
path => '{id}',
method => 'GET',
description => "Get directory mapping.",
permissions => {
check =>['or',
['perm', '/mapping/dir/{id}', ['Mapping.Use']],
['perm', '/mapping/dir/{id}', ['Mapping.Modify']],
['perm', '/mapping/dir/{id}', ['Mapping.Audit']],
],
check => [
'or',
['perm', '/mapping/dir/{id}', ['Mapping.Use']],
['perm', '/mapping/dir/{id}', ['Mapping.Modify']],
['perm', '/mapping/dir/{id}', ['Mapping.Audit']],
],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cfg = PVE::Mapping::Dir::config();
my $id = $param->{id};
my $cfg = PVE::Mapping::Dir::config();
my $id = $param->{id};
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $data = dclone($entry);
my $data = dclone($entry);
$data->{digest} = $cfg->{digest};
$data->{digest} = $cfg->{digest};
return $data;
}});
return $data;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
protected => 1,
path => '',
method => 'POST',
description => "Create a new directory mapping.",
permissions => {
check => ['perm', '/mapping/dir', ['Mapping.Modify']],
check => ['perm', '/mapping/dir', ['Mapping.Modify']],
},
parameters => PVE::Mapping::Dir->createSchema(1),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $id = extract_param($param, 'id');
my $plugin = PVE::Mapping::Dir->lookup('dir');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::Dir->lookup('dir');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $map_list = $opts->{map};
PVE::Mapping::Dir::assert_valid_map_list($map_list);
my $map_list = $opts->{map};
PVE::Mapping::Dir::assert_valid_map_list($map_list);
PVE::Mapping::Dir::lock_dir_config(sub {
my $cfg = PVE::Mapping::Dir::config();
PVE::Mapping::Dir::lock_dir_config(
sub {
my $cfg = PVE::Mapping::Dir::config();
die "dir ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
die "dir ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id} = $opts;
PVE::Mapping::Dir::write_dir_config($cfg);
PVE::Mapping::Dir::write_dir_config($cfg);
}, "create directory mapping failed");
},
"create directory mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
protected => 1,
path => '{id}',
method => 'PUT',
description => "Update a directory mapping.",
permissions => {
check => ['perm', '/mapping/dir/{id}', ['Mapping.Modify']],
check => ['perm', '/mapping/dir/{id}', ['Mapping.Modify']],
},
parameters => PVE::Mapping::Dir->updateSchema(),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
if ($delete) {
$delete = [ PVE::Tools::split_list($delete) ];
}
if ($delete) {
$delete = [PVE::Tools::split_list($delete)];
}
PVE::Mapping::Dir::lock_dir_config(sub {
my $cfg = PVE::Mapping::Dir::config();
PVE::Mapping::Dir::lock_dir_config(
sub {
my $cfg = PVE::Mapping::Dir::config();
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
die "dir ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
die "dir ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
my $plugin = PVE::Mapping::Dir->lookup('dir');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::Dir->lookup('dir');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $map_list = $opts->{map};
PVE::Mapping::Dir::assert_valid_map_list($map_list);
my $map_list = $opts->{map};
PVE::Mapping::Dir::assert_valid_map_list($map_list);
my $data = $cfg->{ids}->{$id};
my $data = $cfg->{ids}->{$id};
my $options = $plugin->private()->{options}->{dir};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
my $options = $plugin->private()->{options}->{dir};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
$data->{$_} = $opts->{$_} for keys $opts->%*;
$data->{$_} = $opts->{$_} for keys $opts->%*;
PVE::Mapping::Dir::write_dir_config($cfg);
PVE::Mapping::Dir::write_dir_config($cfg);
}, "update directory mapping failed");
},
"update directory mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
protected => 1,
path => '{id}',
method => 'DELETE',
description => "Remove directory mapping.",
permissions => {
check => [ 'perm', '/mapping/dir', ['Mapping.Modify']],
check => ['perm', '/mapping/dir', ['Mapping.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = $param->{id};
my $id = $param->{id};
PVE::Mapping::Dir::lock_dir_config(sub {
my $cfg = PVE::Mapping::Dir::config();
PVE::Mapping::Dir::lock_dir_config(
sub {
my $cfg = PVE::Mapping::Dir::config();
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
PVE::Mapping::Dir::write_dir_config($cfg);
PVE::Mapping::Dir::write_dir_config($cfg);
}, "delete dir mapping failed");
},
"delete dir mapping failed",
);
return;
}
return;
},
});
1;

View file

@ -11,289 +11,308 @@ use PVE::Tools qw(extract_param);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
# only proxy if we give the 'check-node' parameter
proxyto_callback => sub {
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
},
description => "List PCI Hardware Mapping",
permissions => {
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or".
" 'Mapping.Audit' permissions on '/mapping/pci/<id>'.",
user => 'all',
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
. " 'Mapping.Audit' permissions on '/mapping/pci/<id>'.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
'check-node' => get_standard_option('pve-node', {
description => "If given, checks the configurations on the given node for ".
"correctness, and adds relevant diagnostics for the devices to the response.",
optional => 1,
}),
},
additionalProperties => 0,
properties => {
'check-node' => get_standard_option(
'pve-node',
{
description => "If given, checks the configurations on the given node for "
. "correctness, and adds relevant diagnostics for the devices to the response.",
optional => 1,
},
),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping."
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
checks => {
type => "array",
optional => 1,
description => "A list of checks, only present if 'check_node' is set.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
enum => ['warning', 'error'],
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
}
},
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping.",
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
checks => {
type => "array",
optional => 1,
description => "A list of checks, only present if 'check_node' is set.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
enum => ['warning', 'error'],
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
},
},
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $check_node = $param->{'check-node'};
my $local_node = PVE::INotify::nodename();
my $check_node = $param->{'check-node'};
my $local_node = PVE::INotify::nodename();
die "wrong node to check - $check_node != $local_node\n"
if defined($check_node) && $check_node ne 'localhost' && $check_node ne $local_node;
die "wrong node to check - $check_node != $local_node\n"
if defined($check_node) && $check_node ne 'localhost' && $check_node ne $local_node;
my $cfg = PVE::Mapping::PCI::config();
my $cfg = PVE::Mapping::PCI::config();
my $can_see_mapping_privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
my $can_see_mapping_privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
my $res = [];
for my $id (keys $cfg->{ids}->%*) {
next if !$rpcenv->check_any($authuser, "/mapping/pci/$id", $can_see_mapping_privs, 1);
next if !$cfg->{ids}->{$id};
my $res = [];
for my $id (keys $cfg->{ids}->%*) {
next
if !$rpcenv->check_any($authuser, "/mapping/pci/$id", $can_see_mapping_privs, 1);
next if !$cfg->{ids}->{$id};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
if (defined($check_node)) {
$entry->{checks} = [];
if (my $mappings = PVE::Mapping::PCI::get_node_mapping($cfg, $id, $check_node)) {
if (!scalar($mappings->@*)) {
push $entry->{checks}->@*, {
severity => 'warning',
message => "No mapping for node $check_node.",
};
}
for my $mapping ($mappings->@*) {
eval { PVE::Mapping::PCI::assert_valid($id, $mapping, $entry) };
if (my $err = $@) {
push $entry->{checks}->@*, {
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
if (defined($check_node)) {
$entry->{checks} = [];
if (
my $mappings = PVE::Mapping::PCI::get_node_mapping($cfg, $id, $check_node)
) {
if (!scalar($mappings->@*)) {
push $entry->{checks}->@*,
{
severity => 'warning',
message => "No mapping for node $check_node.",
};
}
for my $mapping ($mappings->@*) {
eval { PVE::Mapping::PCI::assert_valid($id, $mapping, $entry) };
if (my $err = $@) {
push $entry->{checks}->@*,
{
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
push @$res, $entry;
}
push @$res, $entry;
}
return $res;
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get',
protected => 1,
path => '{id}',
method => 'GET',
description => "Get PCI Mapping.",
permissions => {
check =>['or',
['perm', '/mapping/pci/{id}', ['Mapping.Use']],
['perm', '/mapping/pci/{id}', ['Mapping.Modify']],
['perm', '/mapping/pci/{id}', ['Mapping.Audit']],
],
check => [
'or',
['perm', '/mapping/pci/{id}', ['Mapping.Use']],
['perm', '/mapping/pci/{id}', ['Mapping.Modify']],
['perm', '/mapping/pci/{id}', ['Mapping.Audit']],
],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cfg = PVE::Mapping::PCI::config();
my $id = $param->{id};
my $cfg = PVE::Mapping::PCI::config();
my $id = $param->{id};
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $data = dclone($entry);
my $data = dclone($entry);
$data->{digest} = $cfg->{digest};
$data->{digest} = $cfg->{digest};
return $data;
}});
return $data;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
protected => 1,
path => '',
method => 'POST',
description => "Create a new hardware mapping.",
permissions => {
check => ['perm', '/mapping/pci', ['Mapping.Modify']],
check => ['perm', '/mapping/pci', ['Mapping.Modify']],
},
parameters => PVE::Mapping::PCI->createSchema(1),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $id = extract_param($param, 'id');
my $plugin = PVE::Mapping::PCI->lookup('pci');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::PCI->lookup('pci');
my $opts = $plugin->check_config($id, $param, 1, 1);
PVE::Mapping::PCI::lock_pci_config(sub {
my $cfg = PVE::Mapping::PCI::config();
PVE::Mapping::PCI::lock_pci_config(
sub {
my $cfg = PVE::Mapping::PCI::config();
die "pci ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
die "pci ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id} = $opts;
PVE::Mapping::PCI::write_pci_config($cfg);
PVE::Mapping::PCI::write_pci_config($cfg);
}, "create hardware mapping failed");
},
"create hardware mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
protected => 1,
path => '{id}',
method => 'PUT',
description => "Update a hardware mapping.",
permissions => {
check => ['perm', '/mapping/pci/{id}', ['Mapping.Modify']],
check => ['perm', '/mapping/pci/{id}', ['Mapping.Modify']],
},
parameters => PVE::Mapping::PCI->updateSchema(),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
if ($delete) {
$delete = [ PVE::Tools::split_list($delete) ];
}
if ($delete) {
$delete = [PVE::Tools::split_list($delete)];
}
PVE::Mapping::PCI::lock_pci_config(sub {
my $cfg = PVE::Mapping::PCI::config();
PVE::Mapping::PCI::lock_pci_config(
sub {
my $cfg = PVE::Mapping::PCI::config();
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
die "pci ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
die "pci ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
my $plugin = PVE::Mapping::PCI->lookup('pci');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::PCI->lookup('pci');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $data = $cfg->{ids}->{$id};
my $data = $cfg->{ids}->{$id};
my $options = $plugin->private()->{options}->{pci};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
my $options = $plugin->private()->{options}->{pci};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
$data->{$_} = $opts->{$_} for keys $opts->%*;
$data->{$_} = $opts->{$_} for keys $opts->%*;
PVE::Mapping::PCI::write_pci_config($cfg);
PVE::Mapping::PCI::write_pci_config($cfg);
}, "update hardware mapping failed");
},
"update hardware mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
protected => 1,
path => '{id}',
method => 'DELETE',
description => "Remove Hardware Mapping.",
permissions => {
check => [ 'perm', '/mapping/pci', ['Mapping.Modify']],
check => ['perm', '/mapping/pci', ['Mapping.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = $param->{id};
my $id = $param->{id};
PVE::Mapping::PCI::lock_pci_config(sub {
my $cfg = PVE::Mapping::PCI::config();
PVE::Mapping::PCI::lock_pci_config(
sub {
my $cfg = PVE::Mapping::PCI::config();
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
PVE::Mapping::PCI::write_pci_config($cfg);
PVE::Mapping::PCI::write_pci_config($cfg);
}, "delete pci mapping failed");
},
"delete pci mapping failed",
);
return;
}
return;
},
});
1;

View file

@ -11,287 +11,301 @@ use PVE::Tools qw(extract_param);
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
# only proxy if we give the 'check-node' parameter
proxyto_callback => sub {
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
my ($rpcenv, $proxyto, $param) = @_;
return $param->{'check-node'} // 'localhost';
},
description => "List USB Hardware Mappings",
permissions => {
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or".
" 'Mapping.Audit' permissions on '/mapping/usb/<id>'.",
user => 'all',
description => "Only lists entries where you have 'Mapping.Modify', 'Mapping.Use' or"
. " 'Mapping.Audit' permissions on '/mapping/usb/<id>'.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
'check-node' => get_standard_option('pve-node', {
description => "If given, checks the configurations on the given node for ".
"correctness, and adds relevant errors to the devices.",
optional => 1,
}),
},
additionalProperties => 0,
properties => {
'check-node' => get_standard_option(
'pve-node',
{
description => "If given, checks the configurations on the given node for "
. "correctness, and adds relevant errors to the devices.",
optional => 1,
},
),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping."
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
error => {
description => "A list of errors when 'check_node' is given.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
}
},
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The logical ID of the mapping.",
},
map => {
type => 'array',
description => "The entries of the mapping.",
items => {
type => 'string',
description => "A mapping for a node.",
},
},
description => {
type => 'string',
description => "A description of the logical mapping.",
},
error => {
description => "A list of errors when 'check_node' is given.",
items => {
type => 'object',
properties => {
severity => {
type => "string",
description => "The severity of the error",
},
message => {
type => "string",
description => "The message of the error",
},
},
},
},
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = $param->{'check-node'};
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $node = $param->{'check-node'};
die "Wrong node to check\n"
if defined($node) && $node ne 'localhost' && $node ne PVE::INotify::nodename();
die "Wrong node to check\n"
if defined($node) && $node ne 'localhost' && $node ne PVE::INotify::nodename();
my $cfg = PVE::Mapping::USB::config();
my $cfg = PVE::Mapping::USB::config();
my $res = [];
my $res = [];
my $privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
my $privs = ['Mapping.Modify', 'Mapping.Use', 'Mapping.Audit'];
for my $id (keys $cfg->{ids}->%*) {
next if !$rpcenv->check_full($authuser, "/mapping/usb/$id", $privs, 1, 1);
next if !$cfg->{ids}->{$id};
for my $id (keys $cfg->{ids}->%*) {
next if !$rpcenv->check_full($authuser, "/mapping/usb/$id", $privs, 1, 1);
next if !$cfg->{ids}->{$id};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
my $entry = dclone($cfg->{ids}->{$id});
$entry->{id} = $id;
$entry->{digest} = $cfg->{digest};
if (defined($node)) {
$entry->{errors} = [];
if (my $mappings = PVE::Mapping::USB::get_node_mapping($cfg, $id, $node)) {
if (!scalar($mappings->@*)) {
push $entry->{errors}->@*, {
severity => 'warning',
message => "No mapping for node $node.",
};
}
for my $mapping ($mappings->@*) {
eval {
PVE::Mapping::USB::assert_valid($id, $mapping);
};
if (my $err = $@) {
push $entry->{errors}->@*, {
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
if (defined($node)) {
$entry->{errors} = [];
if (my $mappings = PVE::Mapping::USB::get_node_mapping($cfg, $id, $node)) {
if (!scalar($mappings->@*)) {
push $entry->{errors}->@*,
{
severity => 'warning',
message => "No mapping for node $node.",
};
}
for my $mapping ($mappings->@*) {
eval { PVE::Mapping::USB::assert_valid($id, $mapping); };
if (my $err = $@) {
push $entry->{errors}->@*,
{
severity => 'error',
message => "Invalid configuration: $err",
};
}
}
}
}
push @$res, $entry;
}
push @$res, $entry;
}
return $res;
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get',
protected => 1,
path => '{id}',
method => 'GET',
description => "Get USB Mapping.",
permissions => {
check =>['or',
['perm', '/mapping/usb/{id}', ['Mapping.Audit']],
['perm', '/mapping/usb/{id}', ['Mapping.Use']],
['perm', '/mapping/usb/{id}', ['Mapping.Modify']],
],
check => [
'or',
['perm', '/mapping/usb/{id}', ['Mapping.Audit']],
['perm', '/mapping/usb/{id}', ['Mapping.Use']],
['perm', '/mapping/usb/{id}', ['Mapping.Modify']],
],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cfg = PVE::Mapping::USB::config();
my $id = $param->{id};
my $cfg = PVE::Mapping::USB::config();
my $id = $param->{id};
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $entry = $cfg->{ids}->{$id};
die "mapping '$param->{id}' not found\n" if !defined($entry);
my $data = dclone($entry);
my $data = dclone($entry);
$data->{digest} = $cfg->{digest};
$data->{digest} = $cfg->{digest};
return $data;
}});
return $data;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
protected => 1,
path => '',
method => 'POST',
description => "Create a new hardware mapping.",
permissions => {
check => ['perm', '/mapping/usb', ['Mapping.Modify']],
check => ['perm', '/mapping/usb', ['Mapping.Modify']],
},
parameters => PVE::Mapping::USB->createSchema(1),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $id = extract_param($param, 'id');
my $plugin = PVE::Mapping::USB->lookup('usb');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::USB->lookup('usb');
my $opts = $plugin->check_config($id, $param, 1, 1);
PVE::Mapping::USB::lock_usb_config(sub {
my $cfg = PVE::Mapping::USB::config();
PVE::Mapping::USB::lock_usb_config(
sub {
my $cfg = PVE::Mapping::USB::config();
die "usb ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
die "usb ID '$id' already defined\n" if defined($cfg->{ids}->{$id});
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id} = $opts;
PVE::Mapping::USB::write_usb_config($cfg);
PVE::Mapping::USB::write_usb_config($cfg);
}, "create hardware mapping failed");
},
"create hardware mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
protected => 1,
path => '{id}',
method => 'PUT',
description => "Update a hardware mapping.",
permissions => {
check => ['perm', '/mapping/usb/{id}', ['Mapping.Modify']],
check => ['perm', '/mapping/usb/{id}', ['Mapping.Modify']],
},
parameters => PVE::Mapping::USB->updateSchema(),
returns => {
type => 'null',
type => 'null',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
if ($delete) {
$delete = [ PVE::Tools::split_list($delete) ];
}
if ($delete) {
$delete = [PVE::Tools::split_list($delete)];
}
PVE::Mapping::USB::lock_usb_config(sub {
my $cfg = PVE::Mapping::USB::config();
PVE::Mapping::USB::lock_usb_config(
sub {
my $cfg = PVE::Mapping::USB::config();
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
PVE::Tools::assert_if_modified($cfg->{digest}, $digest) if defined($digest);
die "usb ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
die "usb ID '$id' does not exist\n" if !defined($cfg->{ids}->{$id});
my $plugin = PVE::Mapping::USB->lookup('usb');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $plugin = PVE::Mapping::USB->lookup('usb');
my $opts = $plugin->check_config($id, $param, 1, 1);
my $data = $cfg->{ids}->{$id};
my $data = $cfg->{ids}->{$id};
my $options = $plugin->private()->{options}->{usb};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
my $options = $plugin->private()->{options}->{usb};
PVE::SectionConfig::delete_from_config($data, $options, $opts, $delete);
$data->{$_} = $opts->{$_} for keys $opts->%*;
$data->{$_} = $opts->{$_} for keys $opts->%*;
PVE::Mapping::USB::write_usb_config($cfg);
PVE::Mapping::USB::write_usb_config($cfg);
}, "update hardware mapping failed");
},
"update hardware mapping failed",
);
return;
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
protected => 1,
path => '{id}',
method => 'DELETE',
description => "Remove Hardware Mapping.",
permissions => {
check => [ 'perm', '/mapping/usb', ['Mapping.Modify']],
check => ['perm', '/mapping/usb', ['Mapping.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = $param->{id};
my $id = $param->{id};
PVE::Mapping::USB::lock_usb_config(sub {
my $cfg = PVE::Mapping::USB::config();
PVE::Mapping::USB::lock_usb_config(
sub {
my $cfg = PVE::Mapping::USB::config();
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
if ($cfg->{ids}->{$id}) {
delete $cfg->{ids}->{$id};
}
PVE::Mapping::USB::write_usb_config($cfg);
PVE::Mapping::USB::write_usb_config($cfg);
}, "delete usb mapping failed");
},
"delete usb mapping failed",
);
return;
}
return;
},
});
1;

View file

@ -16,458 +16,474 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "Metrics index.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $result = [
{ name => 'server' },
];
my $result = [
{ name => 'server' },
];
return $result;
}
return $result;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'server_index',
path => 'server',
method => 'GET',
description => "List configured metric servers.",
permissions => {
check => ['perm', '/', ['Sys.Audit']],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => {
description => "The ID of the entry.",
type => 'string'
},
disable => {
description => "Flag to disable the plugin.",
type => 'boolean',
},
type => {
description => "Plugin type.",
type => 'string',
},
server => {
description => "Server dns name or IP address",
type => 'string',
},
port => {
description => "Server network port",
type => 'integer',
},
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => {
description => "The ID of the entry.",
type => 'string',
},
disable => {
description => "Flag to disable the plugin.",
type => 'boolean',
},
type => {
description => "Plugin type.",
type => 'string',
},
server => {
description => "Server dns name or IP address",
type => 'string',
},
port => {
description => "Server network port",
type => 'integer',
},
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [];
my $status_cfg = PVE::Cluster::cfs_read_file('status.cfg');
my $res = [];
my $status_cfg = PVE::Cluster::cfs_read_file('status.cfg');
for my $id (sort keys %{$status_cfg->{ids}}) {
my $plugin_config = $status_cfg->{ids}->{$id};
push @$res, {
id => $id,
disable => $plugin_config->{disable} // 0,
type => $plugin_config->{type},
server => $plugin_config->{server},
port => $plugin_config->{port},
};
}
for my $id (sort keys %{ $status_cfg->{ids} }) {
my $plugin_config = $status_cfg->{ids}->{$id};
push @$res,
{
id => $id,
disable => $plugin_config->{disable} // 0,
type => $plugin_config->{type},
server => $plugin_config->{server},
port => $plugin_config->{port},
};
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'read',
path => 'server/{id}',
method => 'GET',
description => "Read metric server configuration.",
permissions => {
check => ['perm', '/', ['Sys.Audit']],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $status_cfg = PVE::Cluster::cfs_read_file('status.cfg');
my $id = $param->{id};
my $status_cfg = PVE::Cluster::cfs_read_file('status.cfg');
my $id = $param->{id};
if (!defined($status_cfg->{ids}->{$id})) {
die "status server entry '$id' does not exist\n";
}
if (!defined($status_cfg->{ids}->{$id})) {
die "status server entry '$id' does not exist\n";
}
return $status_cfg->{ids}->{$id};
}});
return $status_cfg->{ids}->{$id};
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
path => 'server/{id}',
protected => 1,
method => 'POST',
description => "Create a new external metric server config",
permissions => {
check => ['perm', '/', ['Sys.Modify']],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => PVE::Status::Plugin->createSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $type = extract_param($param, 'type');
my $plugin = PVE::Status::Plugin->lookup($type);
my $id = extract_param($param, 'id');
my $type = extract_param($param, 'type');
my $plugin = PVE::Status::Plugin->lookup($type);
my $id = extract_param($param, 'id');
my $sensitive_params = extract_sensitive_params($param, ['token'], []);
my $sensitive_params = extract_sensitive_params($param, ['token'], []);
PVE::Cluster::cfs_lock_file('status.cfg', undef, sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
PVE::Cluster::cfs_lock_file(
'status.cfg',
undef,
sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
die "Metric server '$id' already exists\n"
if $cfg->{ids}->{$id};
die "Metric server '$id' already exists\n"
if $cfg->{ids}->{$id};
my $opts = $plugin->check_config($id, $param, 1, 1);
my $opts = $plugin->check_config($id, $param, 1, 1);
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id} = $opts;
$plugin->on_add_hook($id, $opts, $sensitive_params);
$plugin->on_add_hook($id, $opts, $sensitive_params);
eval {
$plugin->test_connection($opts, $id);
};
eval { $plugin->test_connection($opts, $id); };
if (my $err = $@) {
eval { $plugin->on_delete_hook($id, $opts) };
warn "$@\n" if $@;
die $err;
}
if (my $err = $@) {
eval { $plugin->on_delete_hook($id, $opts) };
warn "$@\n" if $@;
die $err;
}
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
});
die $@ if $@;
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
},
);
die $@ if $@;
return;
}});
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
protected => 1,
path => 'server/{id}',
method => 'PUT',
description => "Update metric server configuration.",
permissions => {
check => ['perm', '/', ['Sys.Modify']],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => PVE::Status::Plugin->updateSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
if ($delete) {
$delete = [PVE::Tools::split_list($delete)];
}
if ($delete) {
$delete = [PVE::Tools::split_list($delete)];
}
my $sensitive_params = extract_sensitive_params($param, ['token'], $delete);
my $sensitive_params = extract_sensitive_params($param, ['token'], $delete);
PVE::Cluster::cfs_lock_file('status.cfg', undef, sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
PVE::Cluster::cfs_lock_file(
'status.cfg',
undef,
sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
PVE::SectionConfig::assert_if_modified($cfg, $digest);
PVE::SectionConfig::assert_if_modified($cfg, $digest);
my $data = $cfg->{ids}->{$id};
die "no such server '$id'\n" if !$data;
my $data = $cfg->{ids}->{$id};
die "no such server '$id'\n" if !$data;
my $plugin = PVE::Status::Plugin->lookup($data->{type});
my $opts = $plugin->check_config($id, $param, 0, 1);
my $plugin = PVE::Status::Plugin->lookup($data->{type});
my $opts = $plugin->check_config($id, $param, 0, 1);
for my $k (keys %$opts) {
$data->{$k} = $opts->{$k};
}
for my $k (keys %$opts) {
$data->{$k} = $opts->{$k};
}
if ($delete) {
my $options = $plugin->private()->{options}->{$data->{type}};
for my $k (@$delete) {
my $d = $options->{$k} || die "no such option '$k'\n";
die "unable to delete required option '$k'\n" if !$d->{optional};
die "unable to delete fixed option '$k'\n" if $d->{fixed};
die "cannot set and delete property '$k' at the same time!\n"
if defined($opts->{$k});
if ($delete) {
my $options = $plugin->private()->{options}->{ $data->{type} };
for my $k (@$delete) {
my $d = $options->{$k} || die "no such option '$k'\n";
die "unable to delete required option '$k'\n" if !$d->{optional};
die "unable to delete fixed option '$k'\n" if $d->{fixed};
die "cannot set and delete property '$k' at the same time!\n"
if defined($opts->{$k});
delete $data->{$k};
}
}
delete $data->{$k};
}
}
$plugin->on_update_hook($id, $data, $sensitive_params);
$plugin->on_update_hook($id, $data, $sensitive_params);
$plugin->test_connection($data, $id);
$plugin->test_connection($data, $id);
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
});
die $@ if $@;
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
},
);
die $@ if $@;
return;
}});
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
protected => 1,
path => 'server/{id}',
method => 'DELETE',
description => "Remove Metric server.",
permissions => {
check => ['perm', '/', ['Sys.Modify']],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
}
additionalProperties => 0,
properties => {
id => {
type => 'string',
format => 'pve-configid',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Cluster::cfs_lock_file('status.cfg', undef, sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
PVE::Cluster::cfs_lock_file(
'status.cfg',
undef,
sub {
my $cfg = PVE::Cluster::cfs_read_file('status.cfg');
my $id = $param->{id};
my $id = $param->{id};
my $plugin_cfg = $cfg->{ids}->{$id};
my $plugin_cfg = $cfg->{ids}->{$id};
my $plugin = PVE::Status::Plugin->lookup($plugin_cfg->{type});
my $plugin = PVE::Status::Plugin->lookup($plugin_cfg->{type});
$plugin->on_delete_hook($id, $plugin_cfg);
$plugin->on_delete_hook($id, $plugin_cfg);
delete $cfg->{ids}->{$id};
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
});
die $@ if $@;
delete $cfg->{ids}->{$id};
PVE::Cluster::cfs_write_file('status.cfg', $cfg);
},
);
die $@ if $@;
return;
}});
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'export',
path => 'export',
method => 'GET',
protected => 1,
description => "Retrieve metrics of the cluster.",
permissions => {
check => ['perm', '/', ['Sys.Audit']],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
'local-only' => {
type => 'boolean',
description =>
'Only return metrics for the current node instead of the whole cluster',
optional => 1,
default => 0,
},
'start-time' => {
type => 'integer',
description => 'Only include metrics with a timestamp > start-time.',
optional => 1,
default => 0,
},
'history' => {
type => 'boolean',
description => 'Also return historic values.'
. ' Returns full available metric history unless `start-time` is also set',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
'local-only' => {
type => 'boolean',
description =>
'Only return metrics for the current node instead of the whole cluster',
optional => 1,
default => 0,
},
'start-time' => {
type => 'integer',
description => 'Only include metrics with a timestamp > start-time.',
optional => 1,
default => 0,
},
'history' => {
type => 'boolean',
description => 'Also return historic values.'
. ' Returns full available metric history unless `start-time` is also set',
optional => 1,
default => 0,
},
},
},
returns => {
type => 'object',
additionalProperties => 0,
properties => {
data => {
type => 'array',
description => 'Array of system metrics. Metrics are sorted by their timestamp.',
items => {
type => 'object',
additionalProperties => 0,
properties => {
timestamp => {
type => 'integer',
description => 'Time at which this metric was observed',
},
id => {
type => 'string',
description => "Unique identifier for this metric object,"
. " for instance 'node/<nodename>' or"
. " 'qemu/<vmid>'."
},
metric => {
type => 'string',
description => "Name of the metric.",
},
value => {
type => 'number',
description => 'Metric value.',
},
type => {
type => 'string',
description => 'Type of the metric.',
enum => [qw(gauge counter derive)],
}
}
},
type => 'object',
additionalProperties => 0,
properties => {
data => {
type => 'array',
description =>
'Array of system metrics. Metrics are sorted by their timestamp.',
items => {
type => 'object',
additionalProperties => 0,
properties => {
timestamp => {
type => 'integer',
description => 'Time at which this metric was observed',
},
id => {
type => 'string',
description => "Unique identifier for this metric object,"
. " for instance 'node/<nodename>' or"
. " 'qemu/<vmid>'.",
},
metric => {
type => 'string',
description => "Name of the metric.",
},
value => {
type => 'number',
description => 'Metric value.',
},
type => {
type => 'string',
description => 'Type of the metric.',
enum => [qw(gauge counter derive)],
},
},
},
},
},
}
},
},
code => sub {
my ($param) = @_;
my $local_only = $param->{'local-only'} // 0;
my $start = $param->{'start-time'};
my $history = $param->{'history'} // 0;
my ($param) = @_;
my $local_only = $param->{'local-only'} // 0;
my $start = $param->{'start-time'};
my $history = $param->{'history'} // 0;
my $now = time();
my $now = time();
my $generations;
if ($history) {
# Assuming update loop time of pvestatd of 10 seconds.
if (defined($start)) {
my $delta = $now - $start;
$generations = int($delta / 10);
} else {
$generations = PVE::PullMetric::max_generations();
}
my $generations;
if ($history) {
# Assuming update loop time of pvestatd of 10 seconds.
if (defined($start)) {
my $delta = $now - $start;
$generations = int($delta / 10);
} else {
$generations = PVE::PullMetric::max_generations();
}
} else {
$generations = 0;
};
} else {
$generations = 0;
}
my @metrics = @{PVE::PullMetric::get_local_metrics($generations)};
my @metrics = @{ PVE::PullMetric::get_local_metrics($generations) };
if (defined($start)) {
@metrics = grep {
$_->{timestamp} > ($start)
} @metrics;
}
if (defined($start)) {
@metrics = grep {
$_->{timestamp} > ($start)
} @metrics;
}
my $nodename = PVE::INotify::nodename();
my $nodename = PVE::INotify::nodename();
# Fan out to cluster members
# Do NOT remove this check
if (!$local_only) {
my $members = PVE::Cluster::get_members();
# Fan out to cluster members
# Do NOT remove this check
if (!$local_only) {
my $members = PVE::Cluster::get_members();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my ($user, undef) = PVE::AccessControl::split_tokenid($authuser, 1);
my ($user, undef) = PVE::AccessControl::split_tokenid($authuser, 1);
my $ticket;
if ($user) {
# Theoretically, we might now bypass token privilege separation, since
# we use the regular user instead of the token, but
# since we already passed the permission check for this handler,
# this should be fine.
$ticket = PVE::AccessControl::assemble_ticket($user);
} else {
$ticket = PVE::AccessControl::assemble_ticket($authuser);
}
my $ticket;
if ($user) {
# Theoretically, we might now bypass token privilege separation, since
# we use the regular user instead of the token, but
# since we already passed the permission check for this handler,
# this should be fine.
$ticket = PVE::AccessControl::assemble_ticket($user);
} else {
$ticket = PVE::AccessControl::assemble_ticket($authuser);
}
for my $name (keys %$members) {
if ($name eq $nodename) {
# Skip own node, for that one we already have the metrics
next;
}
for my $name (keys %$members) {
if ($name eq $nodename) {
# Skip own node, for that one we already have the metrics
next;
}
if (!$members->{$name}->{online}) {
next;
}
if (!$members->{$name}->{online}) {
next;
}
my $status = eval {
my $fingerprint = PVE::Cluster::get_node_fingerprint($name);
my $ip = scalar(PVE::Cluster::remote_node_ip($name));
my $status = eval {
my $fingerprint = PVE::Cluster::get_node_fingerprint($name);
my $ip = scalar(PVE::Cluster::remote_node_ip($name));
my $conn_args = {
protocol => 'https',
host => $ip,
port => 8006,
ticket => $ticket,
timeout => 5,
};
my $conn_args = {
protocol => 'https',
host => $ip,
port => 8006,
ticket => $ticket,
timeout => 5,
};
$conn_args->{cached_fingerprints} = { $fingerprint => 1 };
$conn_args->{cached_fingerprints} = { $fingerprint => 1 };
my $api_client = PVE::APIClient::LWP->new(%$conn_args);
my $api_client = PVE::APIClient::LWP->new(%$conn_args);
my $params = {
# Do NOT remove 'local-only' - potential for request recursion!
'local-only' => 1,
history => $history,
};
$params->{'start-time'} = $start if defined($start);
my $params = {
# Do NOT remove 'local-only' - potential for request recursion!
'local-only' => 1,
history => $history,
};
$params->{'start-time'} = $start if defined($start);
$api_client->get('/cluster/metrics/export', $params);
};
$api_client->get('/cluster/metrics/export', $params);
};
if ($@) {
syslog('warning', "could not fetch metrics from $name: $@");
} else {
push @metrics, $status->{data}->@*;
}
}
}
if ($@) {
syslog('warning', "could not fetch metrics from $name: $@");
} else {
push @metrics, $status->{data}->@*;
}
}
}
my @sorted = sort {$a->{timestamp} <=> $b->{timestamp}} @metrics;
my @sorted = sort { $a->{timestamp} <=> $b->{timestamp} } @metrics;
return {
data => \@sorted,
};
return {
data => \@sorted,
};
},
});

File diff suppressed because it is too large Load diff

View file

@ -16,54 +16,52 @@ use PVE::API2::HA::Status;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "PVE::API2::HA::Resources",
__PACKAGE__->register_method({
subclass => "PVE::API2::HA::Resources",
path => 'resources',
});
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::HA::Groups",
__PACKAGE__->register_method({
subclass => "PVE::API2::HA::Groups",
path => 'groups',
});
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::HA::Status",
__PACKAGE__->register_method({
subclass => "PVE::API2::HA::Status",
path => 'status',
});
__PACKAGE__->register_method({
name => 'index',
path => '',
name => 'index',
path => '',
method => 'GET',
description => "Directory index.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
check => ['perm', '/', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [
{ id => 'status' },
{ id => 'resources' },
{ id => 'groups' },
];
return $res;
}});
my $res = [
{ id => 'status' }, { id => 'resources' }, { id => 'groups' },
];
return $res;
},
});
1;

View file

@ -11,47 +11,46 @@ use PVE::API2::Hardware::USB;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Hardware::PCI",
path => 'pci',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
subclass => "PVE::API2::Hardware::USB",
path => 'usb',
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "Index of hardware types",
permissions => {
user => 'all',
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => { type => { type => 'string'} },
},
links => [ { rel => 'child', href => "{type}" } ],
type => 'array',
items => {
type => "object",
properties => { type => { type => 'string' } },
},
links => [{ rel => 'child', href => "{type}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [
{ type => 'pci' },
{ type => 'usb' },
];
my $res = [
{ type => 'pci' }, { type => 'usb' },
];
return $res;
}
return $res;
},
});

View file

@ -12,7 +12,7 @@ use base qw(PVE::RESTHandler);
my $default_class_blacklist = "05;06;0b";
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'pci_scan',
path => '',
method => 'GET',
@ -20,154 +20,159 @@ __PACKAGE__->register_method ({
protected => 1,
proxyto => "node",
permissions => {
check => ['perm', '/', ['Sys.Audit', 'Sys.Modify'], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Sys.Modify'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-class-blacklist' => {
type => 'string',
format => 'string-list',
default => $default_class_blacklist,
optional => 1,
description => "A list of blacklisted PCI classes, which will not be returned."
." Following are filtered by default: Memory Controller (05), Bridge (06) and"
." Processor (0b).",
},
verbose => {
type => 'boolean',
default => 1,
optional => 1,
description => "If disabled, does only print the PCI IDs. Otherwise, additional"
." information like vendor and device will be returned.",
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-class-blacklist' => {
type => 'string',
format => 'string-list',
default => $default_class_blacklist,
optional => 1,
description => "A list of blacklisted PCI classes, which will not be returned."
. " Following are filtered by default: Memory Controller (05), Bridge (06) and"
. " Processor (0b).",
},
verbose => {
type => 'boolean',
default => 1,
optional => 1,
description => "If disabled, does only print the PCI IDs. Otherwise, additional"
. " information like vendor and device will be returned.",
},
},
},
returns => {
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The PCI ID.",
},
class => {
type => 'string',
description => 'The PCI Class of the device.',
},
vendor => {
type => 'string',
description => 'The Vendor ID.',
},
vendor_name => {
type => 'string',
optional => 1,
},
device => {
type => 'string',
description => 'The Device ID.',
},
device_name => {
type => 'string',
optional => 1,
},
subsystem_vendor => {
type => 'string',
description => 'The Subsystem Vendor ID.',
optional => 1,
},
subsystem_vendor_name => {
type => 'string',
optional => 1,
},
subsystem_device => {
type => 'string',
description => 'The Subsystem Device ID.',
optional => 1,
},
subsystem_device_name => {
type => 'string',
optional => 1,
},
iommugroup => {
type => 'integer',
description => "The IOMMU group in which the device is in. If no IOMMU group is"
." detected, it is set to -1.",
},
mdev => {
type => 'boolean',
optional => 1,
description => "If set, marks that the device is capable of creating mediated"
." devices.",
},
},
},
links => [{ rel => 'child', href => "{id}" }],
type => 'array',
items => {
type => "object",
properties => {
id => {
type => 'string',
description => "The PCI ID.",
},
class => {
type => 'string',
description => 'The PCI Class of the device.',
},
vendor => {
type => 'string',
description => 'The Vendor ID.',
},
vendor_name => {
type => 'string',
optional => 1,
},
device => {
type => 'string',
description => 'The Device ID.',
},
device_name => {
type => 'string',
optional => 1,
},
subsystem_vendor => {
type => 'string',
description => 'The Subsystem Vendor ID.',
optional => 1,
},
subsystem_vendor_name => {
type => 'string',
optional => 1,
},
subsystem_device => {
type => 'string',
description => 'The Subsystem Device ID.',
optional => 1,
},
subsystem_device_name => {
type => 'string',
optional => 1,
},
iommugroup => {
type => 'integer',
description =>
"The IOMMU group in which the device is in. If no IOMMU group is"
. " detected, it is set to -1.",
},
mdev => {
type => 'boolean',
optional => 1,
description =>
"If set, marks that the device is capable of creating mediated"
. " devices.",
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $blacklist = $param->{'pci-class-blacklist'} // $default_class_blacklist;
my $class_regex = join('|', PVE::Tools::split_list($blacklist));
my $blacklist = $param->{'pci-class-blacklist'} // $default_class_blacklist;
my $class_regex = join('|', PVE::Tools::split_list($blacklist));
my $filter;
my $filter;
if ($class_regex ne '') {
$filter = sub {
my ($pcidevice) = @_;
if ($class_regex ne '') {
$filter = sub {
my ($pcidevice) = @_;
if ($pcidevice->{class} =~ m/^0x(?:$class_regex)/) {
return 0;
}
if ($pcidevice->{class} =~ m/^0x(?:$class_regex)/) {
return 0;
}
return 1;
};
}
return 1;
};
}
my $verbose = $param->{verbose} // 1;
my $verbose = $param->{verbose} // 1;
return PVE::SysFSTools::lspci($filter, $verbose);
}});
return PVE::SysFSTools::lspci($filter, $verbose);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'pci_index',
path => '{pci-id-or-mapping}',
method => 'GET',
description => "Index of available pci methods",
permissions => {
user => 'all',
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-id-or-mapping' => {
type => 'string',
pattern => '(?:(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])|([a-zA-Z][a-zA-Z0-9_-]+)',
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-id-or-mapping' => {
type => 'string',
pattern =>
'(?:(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])|([a-zA-Z][a-zA-Z0-9_-]+)',
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => { method => { type => 'string'} },
},
links => [ { rel => 'child', href => "{method}" } ],
type => 'array',
items => {
type => "object",
properties => { method => { type => 'string' } },
},
links => [{ rel => 'child', href => "{method}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [
{ method => 'mdev' },
];
my $res = [
{ method => 'mdev' },
];
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'mdevscan',
path => '{pci-id-or-mapping}/mdev',
method => 'GET',
@ -175,69 +180,73 @@ __PACKAGE__->register_method ({
protected => 1,
proxyto => "node",
permissions => {
check => ['perm', '/', ['Sys.Audit', 'Sys.Modify'], any => 1],
check => ['perm', '/', ['Sys.Audit', 'Sys.Modify'], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-id-or-mapping' => {
type => 'string',
pattern => '(?:(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])|([a-zA-Z][a-zA-Z0-9_-]+)',
description => "The PCI ID or mapping to list the mdev types for."
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
'pci-id-or-mapping' => {
type => 'string',
pattern =>
'(?:(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F])|([a-zA-Z][a-zA-Z0-9_-]+)',
description => "The PCI ID or mapping to list the mdev types for.",
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
type => {
type => 'string',
description => "The name of the mdev type.",
},
available => {
type => 'integer',
description => "The number of still available instances of this type.",
},
description => {
type => 'string',
description => "Additional description of the type."
},
name => {
type => 'string',
optional => 1,
description => 'A human readable name for the type.',
},
},
},
type => 'array',
items => {
type => "object",
properties => {
type => {
type => 'string',
description => "The name of the mdev type.",
},
available => {
type => 'integer',
description => "The number of still available instances of this type.",
},
description => {
type => 'string',
description => "Additional description of the type.",
},
name => {
type => 'string',
optional => 1,
description => 'A human readable name for the type.',
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
if ($param->{'pci-id-or-mapping'} =~ m/^(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$/) {
return PVE::SysFSTools::get_mdev_types($param->{'pci-id-or-mapping'}); # PCI ID
} else {
my $mapping = $param->{'pci-id-or-mapping'};
if ($param->{'pci-id-or-mapping'} =~
m/^(?:[0-9a-fA-F]{4}:)?[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-9a-fA-F]$/
) {
return PVE::SysFSTools::get_mdev_types($param->{'pci-id-or-mapping'}); # PCI ID
} else {
my $mapping = $param->{'pci-id-or-mapping'};
my $types = {};
my $devices = PVE::Mapping::PCI::find_on_current_node($mapping);
for my $device ($devices->@*) {
my $id = $device->{path};
next if $id =~ m/;/; # mdev not supported for multifunction devices
my $types = {};
my $devices = PVE::Mapping::PCI::find_on_current_node($mapping);
for my $device ($devices->@*) {
my $id = $device->{path};
next if $id =~ m/;/; # mdev not supported for multifunction devices
my $device_types = PVE::SysFSTools::get_mdev_types($id);
my $device_types = PVE::SysFSTools::get_mdev_types($id);
for my $type_definition ($device_types->@*) {
my $type = $type_definition->{type};
if (!defined($types->{$type})) {
$types->{$type} = $type_definition;
}
}
}
for my $type_definition ($device_types->@*) {
my $type = $type_definition->{type};
if (!defined($types->{$type})) {
$types->{$type} = $type_definition;
}
}
}
return [sort { $a->{type} cmp $b->{type} } values($types->%*)];
}
return [sort { $a->{type} cmp $b->{type} } values($types->%*)];
}
}});
},
});

View file

@ -17,37 +17,37 @@ __PACKAGE__->register_method({
protected => 1,
proxyto => "node",
permissions => {
check => ['perm', '/', ['Sys.Modify']],
check => ['perm', '/', ['Sys.Modify']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
busnum => { type => 'integer'},
class => { type => 'integer'},
devnum => { type => 'integer'},
level => { type => 'integer'},
manufacturer => { type => 'string', optional => 1 },
port => { type => 'integer'},
prodid => { type => 'string'},
product => { type => 'string', optional => 1 },
serial => { type => 'string', optional => 1 },
speed => { type => 'string'},
usbpath => { type => 'string', optional => 1},
vendid => { type => 'string'},
},
},
type => 'array',
items => {
type => "object",
properties => {
busnum => { type => 'integer' },
class => { type => 'integer' },
devnum => { type => 'integer' },
level => { type => 'integer' },
manufacturer => { type => 'string', optional => 1 },
port => { type => 'integer' },
prodid => { type => 'string' },
product => { type => 'string', optional => 1 },
serial => { type => 'string', optional => 1 },
speed => { type => 'string' },
usbpath => { type => 'string', optional => 1 },
vendid => { type => 'string' },
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return PVE::SysFSTools::scan_usb();
}
return PVE::SysFSTools::scan_usb();
},
});

File diff suppressed because it is too large Load diff

View file

@ -10,21 +10,23 @@ use PVE::Tools qw(extract_param);
use base qw(PVE::RESTHandler);
my $node_config_schema = PVE::NodeConfig::get_nodeconfig_schema();
my $node_config_keys = [ sort keys %$node_config_schema ];
my $node_config_keys = [sort keys %$node_config_schema];
my $node_config_return_properties = {
digest => {
type => 'string',
description => 'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
maxLength => 40,
optional => 1,
type => 'string',
description =>
'Prevent changes if current configuration file has different SHA1 digest. This can be used to prevent concurrent modifications.',
maxLength => 40,
optional => 1,
},
%$node_config_schema,
};
my $node_config_properties = {
delete => {
type => 'string', format => 'pve-configid-list',
description => "A list of settings you want to delete.",
optional => 1,
type => 'string',
format => 'pve-configid-list',
description => "A list of settings you want to delete.",
optional => 1,
},
node => get_standard_option('pve-node'),
%$node_config_return_properties,
@ -36,38 +38,39 @@ __PACKAGE__->register_method({
method => 'GET',
description => "Get node configuration options.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
check => ['perm', '/', ['Sys.Audit']],
},
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
property => {
type => 'string',
description => 'Return only a specific property from the node configuration.',
enum => $node_config_keys,
optional => 1,
default => 'all',
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
property => {
type => 'string',
description => 'Return only a specific property from the node configuration.',
enum => $node_config_keys,
optional => 1,
default => 'all',
},
},
},
returns => {
type => "object",
properties => $node_config_return_properties,
type => "object",
properties => $node_config_return_properties,
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $config = PVE::NodeConfig::load_config($param->{node});
my $config = PVE::NodeConfig::load_config($param->{node});
if (defined (my $prop = $param->{property})) {
return {} if !exists $config->{$prop};
return { $prop => $config->{$prop} };
}
if (defined(my $prop = $param->{property})) {
return {} if !exists $config->{$prop};
return { $prop => $config->{$prop} };
}
return $config;
}});
return $config;
},
});
__PACKAGE__->register_method({
name => 'set_options',
@ -75,43 +78,44 @@ __PACKAGE__->register_method({
method => 'PUT',
description => "Set node configuration options.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
check => ['perm', '/', ['Sys.Modify']],
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => $node_config_properties,
additionalProperties => 0,
properties => $node_config_properties,
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $delete = extract_param($param, 'delete');
my $node = extract_param($param, 'node');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $node = extract_param($param, 'node');
my $digest = extract_param($param, 'digest');
my $code = sub {
my $conf = PVE::NodeConfig::load_config($node);
my $code = sub {
my $conf = PVE::NodeConfig::load_config($node);
PVE::Tools::assert_if_modified($digest, $conf->{digest});
PVE::Tools::assert_if_modified($digest, $conf->{digest});
foreach my $opt (sort keys %$param) {
$conf->{$opt} = $param->{$opt};
}
foreach my $opt (sort keys %$param) {
$conf->{$opt} = $param->{$opt};
}
foreach my $opt (PVE::Tools::split_list($delete)) {
delete $conf->{$opt};
};
foreach my $opt (PVE::Tools::split_list($delete)) {
delete $conf->{$opt};
}
PVE::NodeConfig::verify_conf($conf);
PVE::NodeConfig::write_config($node, $conf);
};
PVE::NodeConfig::verify_conf($conf);
PVE::NodeConfig::write_config($node, $conf);
};
PVE::NodeConfig::lock_config($node, $code);
die $@ if $@;
PVE::NodeConfig::lock_config($node, $code);
die $@ if $@;
return undef;
}});
return undef;
},
});
1;

File diff suppressed because it is too large Load diff

View file

@ -16,487 +16,521 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "List pools or get pool configuration.",
permissions => {
description => "List all pools where you have Pool.Audit permissions on /pool/<pool>, or the pool specific with {poolid}",
user => 'all',
description =>
"List all pools where you have Pool.Audit permissions on /pool/<pool>, or the pool specific with {poolid}",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
optional => 1,
},
type => {
type => 'string',
enum => [ 'qemu', 'lxc', 'storage' ],
optional => 1,
requires => 'poolid',
},
},
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
optional => 1,
},
type => {
type => 'string',
enum => ['qemu', 'lxc', 'storage'],
optional => 1,
requires => 'poolid',
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
poolid => { type => 'string' },
comment => {
type => 'string',
optional => 1,
},
members => {
type => 'array',
optional => 1,
items => {
type => "object",
additionalProperties => 1,
properties => {
type => {
type => 'string',
enum => [ 'qemu', 'lxc', 'openvz', 'storage' ],
},
id => {
type => 'string',
},
node => {
type => 'string',
},
vmid => {
type => 'integer',
optional => 1,
},
storage => {
type => 'string',
optional => 1,
},
},
},
},
},
},
links => [ { rel => 'child', href => "{poolid}" } ],
type => 'array',
items => {
type => "object",
properties => {
poolid => { type => 'string' },
comment => {
type => 'string',
optional => 1,
},
members => {
type => 'array',
optional => 1,
items => {
type => "object",
additionalProperties => 1,
properties => {
type => {
type => 'string',
enum => ['qemu', 'lxc', 'openvz', 'storage'],
},
id => {
type => 'string',
},
node => {
type => 'string',
},
vmid => {
type => 'integer',
optional => 1,
},
storage => {
type => 'string',
optional => 1,
},
},
},
},
},
},
links => [{ rel => 'child', href => "{poolid}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $usercfg = $rpcenv->{user_cfg};
my $usercfg = $rpcenv->{user_cfg};
my $res = [];
if (my $poolid = $param->{poolid}) {
$rpcenv->check($authuser, "/pool/$poolid", [ 'Pool.Audit' ], 1);
my $res = [];
if (my $poolid = $param->{poolid}) {
$rpcenv->check($authuser, "/pool/$poolid", ['Pool.Audit'], 1);
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
my $rrd = PVE::Cluster::rrd_dump();
my $rrd = PVE::Cluster::rrd_dump();
my $pool_config = $usercfg->{pools}->{$poolid};
my $pool_config = $usercfg->{pools}->{$poolid};
die "pool '$poolid' does not exist\n" if !$pool_config;
die "pool '$poolid' does not exist\n" if !$pool_config;
my $members = [];
for my $vmid (sort keys %{$pool_config->{vms}}) {
my $vmdata = $idlist->{$vmid};
next if !$vmdata || defined($param->{type}) && $param->{type} ne $vmdata->{type};
my $entry = PVE::API2Tools::extract_vm_stats($vmid, $vmdata, $rrd);
push @$members, $entry;
}
my $members = [];
for my $vmid (sort keys %{ $pool_config->{vms} }) {
my $vmdata = $idlist->{$vmid};
next
if !$vmdata || defined($param->{type}) && $param->{type} ne $vmdata->{type};
my $entry = PVE::API2Tools::extract_vm_stats($vmid, $vmdata, $rrd);
push @$members, $entry;
}
my $nodename = PVE::INotify::nodename();
my $cfg = PVE::Storage::config();
if (!defined($param->{type}) || $param->{type} eq 'storage') {
for my $storeid (sort keys %{$pool_config->{storage}}) {
my $scfg = PVE::Storage::storage_config ($cfg, $storeid, 1);
next if !$scfg;
my $nodename = PVE::INotify::nodename();
my $cfg = PVE::Storage::config();
if (!defined($param->{type}) || $param->{type} eq 'storage') {
for my $storeid (sort keys %{ $pool_config->{storage} }) {
my $scfg = PVE::Storage::storage_config($cfg, $storeid, 1);
next if !$scfg;
my $storage_node = $nodename; # prefer local node
if ($scfg->{nodes} && !$scfg->{nodes}->{$storage_node}) {
for my $node (sort keys(%{$scfg->{nodes}})) {
$storage_node = $node;
last;
}
}
my $storage_node = $nodename; # prefer local node
if ($scfg->{nodes} && !$scfg->{nodes}->{$storage_node}) {
for my $node (sort keys(%{ $scfg->{nodes} })) {
$storage_node = $node;
last;
}
}
my $entry = PVE::API2Tools::extract_storage_stats($storeid, $scfg, $storage_node, $rrd);
push @$members, $entry;
}
}
my $entry =
PVE::API2Tools::extract_storage_stats($storeid, $scfg, $storage_node, $rrd);
push @$members, $entry;
}
}
my $pool_info = {
members => $members,
};
$pool_info->{comment} = $pool_config->{comment} if defined($pool_config->{comment});
$pool_info->{poolid} = $poolid;
my $pool_info = {
members => $members,
};
$pool_info->{comment} = $pool_config->{comment} if defined($pool_config->{comment});
$pool_info->{poolid} = $poolid;
push @$res, $pool_info;
} else {
for my $pool (sort keys %{$usercfg->{pools}}) {
next if !$rpcenv->check($authuser, "/pool/$pool", [ 'Pool.Audit' ], 1);
push @$res, $pool_info;
} else {
for my $pool (sort keys %{ $usercfg->{pools} }) {
next if !$rpcenv->check($authuser, "/pool/$pool", ['Pool.Audit'], 1);
my $entry = { poolid => $pool };
my $pool_config = $usercfg->{pools}->{$pool};
$entry->{comment} = $pool_config->{comment} if defined($pool_config->{comment});
push @$res, $entry;
}
}
my $entry = { poolid => $pool };
my $pool_config = $usercfg->{pools}->{$pool};
$entry->{comment} = $pool_config->{comment} if defined($pool_config->{comment});
push @$res, $entry;
}
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create_pool',
protected => 1,
path => '',
method => 'POST',
permissions => {
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
},
description => "Create new pool.",
parameters => {
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
},
comment => {
type => 'string',
optional => 1,
},
},
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
},
comment => {
type => 'string',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::AccessControl::lock_user_config(sub {
my $usercfg = cfs_read_file("user.cfg");
my $pool = $param->{poolid};
PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg");
my $pool = $param->{poolid};
die "pool '$pool' already exists\n" if $usercfg->{pools}->{$pool};
if ($pool =~ m!^(.*)/[^/]+$!) {
my $parent = $1;
die "parent '$parent' of pool '$pool' does not exist\n"
if !defined($usercfg->{pools}->{$parent});
}
die "pool '$pool' already exists\n" if $usercfg->{pools}->{$pool};
if ($pool =~ m!^(.*)/[^/]+$!) {
my $parent = $1;
die "parent '$parent' of pool '$pool' does not exist\n"
if !defined($usercfg->{pools}->{$parent});
}
$usercfg->{pools}->{$pool} = {
vms => {},
storage => {},
};
$usercfg->{pools}->{$pool} = {
vms => {},
storage => {},
};
$usercfg->{pools}->{$pool}->{comment} = $param->{comment} if $param->{comment};
$usercfg->{pools}->{$pool}->{comment} = $param->{comment} if $param->{comment};
cfs_write_file("user.cfg", $usercfg);
}, "create pool failed");
cfs_write_file("user.cfg", $usercfg);
},
"create pool failed",
);
return;
}});
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update_pool_deprecated',
protected => 1,
path => '{poolid}',
method => 'PUT',
permissions => {
description => "You also need the right to modify permissions on any object you add/delete.",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
description =>
"You also need the right to modify permissions on any object you add/delete.",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
},
description => "Update pool data (deprecated, no support for nested pools - use 'PUT /pools/?poolid={poolid}' instead).",
description =>
"Update pool data (deprecated, no support for nested pools - use 'PUT /pools/?poolid={poolid}' instead).",
parameters => {
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
comment => { type => 'string', optional => 1 },
vms => {
description => 'List of guest VMIDs to add or remove from this pool.',
type => 'string', format => 'pve-vmid-list',
optional => 1,
},
storage => {
description => 'List of storage IDs to add or remove from this pool.',
type => 'string', format => 'pve-storage-id-list',
optional => 1,
},
'allow-move' => {
description => 'Allow adding a guest even if already in another pool.'
.' The guest will be removed from its current pool and added to this one.',
type => 'boolean',
optional => 1,
default => 0,
},
delete => {
description => 'Remove the passed VMIDs and/or storage IDs instead of adding them.',
type => 'boolean',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
comment => { type => 'string', optional => 1 },
vms => {
description => 'List of guest VMIDs to add or remove from this pool.',
type => 'string',
format => 'pve-vmid-list',
optional => 1,
},
storage => {
description => 'List of storage IDs to add or remove from this pool.',
type => 'string',
format => 'pve-storage-id-list',
optional => 1,
},
'allow-move' => {
description => 'Allow adding a guest even if already in another pool.'
. ' The guest will be removed from its current pool and added to this one.',
type => 'boolean',
optional => 1,
default => 0,
},
delete => {
description =>
'Remove the passed VMIDs and/or storage IDs instead of adding them.',
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
return __PACKAGE__->update_pool($param);
}});
return __PACKAGE__->update_pool($param);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update_pool',
protected => 1,
path => '',
method => 'PUT',
permissions => {
description => "You also need the right to modify permissions on any object you add/delete.",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
description =>
"You also need the right to modify permissions on any object you add/delete.",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
},
description => "Update pool.",
parameters => {
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
comment => { type => 'string', optional => 1 },
vms => {
description => 'List of guest VMIDs to add or remove from this pool.',
type => 'string', format => 'pve-vmid-list',
optional => 1,
},
storage => {
description => 'List of storage IDs to add or remove from this pool.',
type => 'string', format => 'pve-storage-id-list',
optional => 1,
},
'allow-move' => {
description => 'Allow adding a guest even if already in another pool.'
.' The guest will be removed from its current pool and added to this one.',
type => 'boolean',
optional => 1,
default => 0,
},
delete => {
description => 'Remove the passed VMIDs and/or storage IDs instead of adding them.',
type => 'boolean',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
comment => { type => 'string', optional => 1 },
vms => {
description => 'List of guest VMIDs to add or remove from this pool.',
type => 'string',
format => 'pve-vmid-list',
optional => 1,
},
storage => {
description => 'List of storage IDs to add or remove from this pool.',
type => 'string',
format => 'pve-storage-id-list',
optional => 1,
},
'allow-move' => {
description => 'Allow adding a guest even if already in another pool.'
. ' The guest will be removed from its current pool and added to this one.',
type => 'boolean',
optional => 1,
default => 0,
},
delete => {
description =>
'Remove the passed VMIDs and/or storage IDs instead of adding them.',
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
PVE::AccessControl::lock_user_config(sub {
my $usercfg = cfs_read_file("user.cfg");
my $pool = $param->{poolid};
my $pool_config = $usercfg->{pools}->{$pool};
PVE::AccessControl::lock_user_config(
sub {
my $usercfg = cfs_read_file("user.cfg");
my $pool = $param->{poolid};
my $pool_config = $usercfg->{pools}->{$pool};
die "pool '$pool' does not exist\n" if !$pool_config;
die "pool '$pool' does not exist\n" if !$pool_config;
$pool_config->{comment} = $param->{comment} if defined($param->{comment});
$pool_config->{comment} = $param->{comment} if defined($param->{comment});
if (defined($param->{vms})) {
for my $vmid (PVE::Tools::split_list($param->{vms})) {
$rpcenv->check_perm_modify($authuser, "/vms/$vmid");
if (defined($param->{vms})) {
for my $vmid (PVE::Tools::split_list($param->{vms})) {
$rpcenv->check_perm_modify($authuser, "/vms/$vmid");
if ($param->{delete}) {
die "VM $vmid is not a pool member\n" if !$pool_config->{vms}->{$vmid};
delete $pool_config->{vms}->{$vmid};
delete $usercfg->{vms}->{$vmid};
} else {
die "VM $vmid is already a pool member\n" if $pool_config->{vms}->{$vmid};
if (defined(my $existing_pool = $usercfg->{vms}->{$vmid})) {
die "VM $vmid belongs already to pool '$existing_pool' and 'allow-move' is not set\n"
if !$param->{'allow-move'};
if ($param->{delete}) {
die "VM $vmid is not a pool member\n"
if !$pool_config->{vms}->{$vmid};
delete $pool_config->{vms}->{$vmid};
delete $usercfg->{vms}->{$vmid};
} else {
die "VM $vmid is already a pool member\n"
if $pool_config->{vms}->{$vmid};
if (defined(my $existing_pool = $usercfg->{vms}->{$vmid})) {
die
"VM $vmid belongs already to pool '$existing_pool' and 'allow-move' is not set\n"
if !$param->{'allow-move'};
$rpcenv->check($authuser, "/pool/$existing_pool", ['Pool.Allocate']);
delete $usercfg->{pools}->{$existing_pool}->{vms}->{$vmid};
}
$pool_config->{vms}->{$vmid} = 1;
$usercfg->{vms}->{$vmid} = $pool;
}
}
}
$rpcenv->check(
$authuser, "/pool/$existing_pool", ['Pool.Allocate'],
);
delete $usercfg->{pools}->{$existing_pool}->{vms}->{$vmid};
}
$pool_config->{vms}->{$vmid} = 1;
$usercfg->{vms}->{$vmid} = $pool;
}
}
}
if (defined($param->{storage})) {
for my $storeid (PVE::Tools::split_list($param->{storage})) {
$rpcenv->check_perm_modify($authuser, "/storage/$storeid");
if (defined($param->{storage})) {
for my $storeid (PVE::Tools::split_list($param->{storage})) {
$rpcenv->check_perm_modify($authuser, "/storage/$storeid");
if ($param->{delete}) {
die "Storage '$storeid' is not a pool member\n"
if !$pool_config->{storage}->{$storeid};
delete $pool_config->{storage}->{$storeid};
} else {
die "Storage '$storeid' is already a pool member\n"
if $pool_config->{storage}->{$storeid};
if ($param->{delete}) {
die "Storage '$storeid' is not a pool member\n"
if !$pool_config->{storage}->{$storeid};
delete $pool_config->{storage}->{$storeid};
} else {
die "Storage '$storeid' is already a pool member\n"
if $pool_config->{storage}->{$storeid};
$pool_config->{storage}->{$storeid} = 1;
}
}
}
$pool_config->{storage}->{$storeid} = 1;
}
}
}
cfs_write_file("user.cfg", $usercfg);
}, "update pools failed");
cfs_write_file("user.cfg", $usercfg);
},
"update pools failed",
);
return;
}});
return;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'read_pool',
path => '{poolid}',
method => 'GET',
permissions => {
check => ['perm', '/pool/{poolid}', ['Pool.Audit']],
check => ['perm', '/pool/{poolid}', ['Pool.Audit']],
},
description => "Get pool configuration (deprecated, no support for nested pools, use 'GET /pools/?poolid={poolid}').",
description =>
"Get pool configuration (deprecated, no support for nested pools, use 'GET /pools/?poolid={poolid}').",
parameters => {
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
},
type => {
type => 'string',
enum => [ 'qemu', 'lxc', 'storage' ],
optional => 1,
},
},
additionalProperties => 0,
properties => {
poolid => {
type => 'string',
format => 'pve-poolid',
},
type => {
type => 'string',
enum => ['qemu', 'lxc', 'storage'],
optional => 1,
},
},
},
returns => {
type => "object",
additionalProperties => 0,
properties => {
comment => {
type => 'string',
optional => 1,
},
members => {
type => 'array',
items => {
type => "object",
additionalProperties => 1,
properties => {
type => {
type => 'string',
enum => [ 'qemu', 'lxc', 'openvz', 'storage' ],
},
id => {
type => 'string',
},
node => {
type => 'string',
},
vmid => {
type => 'integer',
optional => 1,
},
storage => {
type => 'string',
optional => 1,
},
},
},
},
},
type => "object",
additionalProperties => 0,
properties => {
comment => {
type => 'string',
optional => 1,
},
members => {
type => 'array',
items => {
type => "object",
additionalProperties => 1,
properties => {
type => {
type => 'string',
enum => ['qemu', 'lxc', 'openvz', 'storage'],
},
id => {
type => 'string',
},
node => {
type => 'string',
},
vmid => {
type => 'integer',
optional => 1,
},
storage => {
type => 'string',
optional => 1,
},
},
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $pool_info = __PACKAGE__->index($param);
return $pool_info->[0];
}});
my $pool_info = __PACKAGE__->index($param);
return $pool_info->[0];
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete_pool_deprecated',
protected => 1,
path => '{poolid}',
method => 'DELETE',
permissions => {
description => "You can only delete empty pools (no members).",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
description => "You can only delete empty pools (no members).",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
},
description => "Delete pool (deprecated, no support for nested pools, use 'DELETE /pools/?poolid={poolid}').",
description =>
"Delete pool (deprecated, no support for nested pools, use 'DELETE /pools/?poolid={poolid}').",
parameters => {
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
}
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
return __PACKAGE__->delete_pool($param);
}});
return __PACKAGE__->delete_pool($param);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete_pool',
protected => 1,
path => '',
method => 'DELETE',
permissions => {
description => "You can only delete empty pools (no members).",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
description => "You can only delete empty pools (no members).",
check => ['perm', '/pool/{poolid}', ['Pool.Allocate']],
},
description => "Delete pool.",
parameters => {
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
}
additionalProperties => 0,
properties => {
poolid => { type => 'string', format => 'pve-poolid' },
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
PVE::AccessControl::lock_user_config(sub {
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
PVE::AccessControl::lock_user_config(
sub {
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
my $storecfg = PVE::Storage::config();
my $usercfg = cfs_read_file("user.cfg");
my $storecfg = PVE::Storage::config();
my $usercfg = cfs_read_file("user.cfg");
my $pool = $param->{poolid};
my $pool = $param->{poolid};
my $pool_config = $usercfg->{pools}->{$pool};
die "pool '$pool' does not exist\n" if !$pool_config;
for my $subpool (sort keys %{$pool_config->{pools}}) {
die "pool '$pool' is not empty (contains pool '$subpool')\n";
}
my $pool_config = $usercfg->{pools}->{$pool};
die "pool '$pool' does not exist\n" if !$pool_config;
for my $subpool (sort keys %{ $pool_config->{pools} }) {
die "pool '$pool' is not empty (contains pool '$subpool')\n";
}
for my $vmid (sort keys %{$pool_config->{vms}}) {
next if !$idlist->{$vmid}; # ignore destroyed guests
die "pool '$pool' is not empty (contains VM $vmid)\n";
}
for my $vmid (sort keys %{ $pool_config->{vms} }) {
next if !$idlist->{$vmid}; # ignore destroyed guests
die "pool '$pool' is not empty (contains VM $vmid)\n";
}
for my $storeid (sort keys %{$pool_config->{storage}}) {
next if !PVE::Storage::storage_config ($storecfg, $storeid, 1);
die "pool '$pool' is not empty (contains storage '$storeid')\n";
}
for my $storeid (sort keys %{ $pool_config->{storage} }) {
next if !PVE::Storage::storage_config($storecfg, $storeid, 1);
die "pool '$pool' is not empty (contains storage '$storeid')\n";
}
delete ($usercfg->{pools}->{$pool});
PVE::AccessControl::delete_pool_acl($pool, $usercfg);
delete($usercfg->{pools}->{$pool});
PVE::AccessControl::delete_pool_acl($pool, $usercfg);
cfs_write_file("user.cfg", $usercfg);
}, "delete pool failed");
cfs_write_file("user.cfg", $usercfg);
},
"delete pool failed",
);
return;
}});
return;
},
});
1;

View file

@ -27,11 +27,11 @@ our $lookup_guest_class = sub {
my ($vmtype) = @_;
if ($vmtype eq 'qemu') {
return 'PVE::QemuConfig';
return 'PVE::QemuConfig';
} elsif ($vmtype eq 'lxc') {
return 'PVE::LXC::Config';
return 'PVE::LXC::Config';
} else {
die "unknown guest type '$vmtype' - internal error";
die "unknown guest type '$vmtype' - internal error";
}
};
@ -42,38 +42,37 @@ sub run_single_job {
my $local_node = PVE::INotify::nodename();
my $code = sub {
$now //= time();
$now //= time();
my $cfg = PVE::ReplicationConfig->new();
my $cfg = PVE::ReplicationConfig->new();
my $jobcfg = $cfg->{ids}->{$jobid};
die "no such job '$jobid'\n" if !$jobcfg;
my $jobcfg = $cfg->{ids}->{$jobid};
die "no such job '$jobid'\n" if !$jobcfg;
die "internal error - not implemented" if $jobcfg->{type} ne 'local';
die "internal error - not implemented" if $jobcfg->{type} ne 'local';
die "job '$jobid' is disabled\n" if $jobcfg->{disable};
die "job '$jobid' is disabled\n" if $jobcfg->{disable};
my $vms = PVE::Cluster::get_vmlist();
my $vmid = $jobcfg->{guest};
my $vms = PVE::Cluster::get_vmlist();
my $vmid = $jobcfg->{guest};
die "no such guest '$vmid'\n" if !$vms->{ids}->{$vmid};
die "no such guest '$vmid'\n" if !$vms->{ids}->{$vmid};
die "guest '$vmid' is not on local node\n"
if $vms->{ids}->{$vmid}->{node} ne $local_node;
die "guest '$vmid' is not on local node\n"
if $vms->{ids}->{$vmid}->{node} ne $local_node;
die "unable to sync to local node\n" if $jobcfg->{target} eq $local_node;
die "unable to sync to local node\n" if $jobcfg->{target} eq $local_node;
my $vmtype = $vms->{ids}->{$vmid}->{type};
my $vmtype = $vms->{ids}->{$vmid}->{type};
my $guest_class = $lookup_guest_class->($vmtype);
PVE::Replication::run_replication($guest_class, $jobcfg, $now, $now, $logfunc);
my $guest_class = $lookup_guest_class->($vmtype);
PVE::Replication::run_replication($guest_class, $jobcfg, $now, $now, $logfunc);
};
my $res = PVE::Tools::lock_file($pvesr_lock_path, 60, $code);
die $@ if $@;
}
# TODO: below two should probably part of the general job framework/plugin system
my sub _should_mail_at_failcount {
my ($fail_count) = @_;
@ -87,11 +86,10 @@ my sub _should_mail_at_failcount {
# failing job is re-tried every half hour, try to send one mail after 1, 2, 4, 8, etc. days
my $i = 1;
while ($i * 48 < $fail_count) {
$i = $i * 2;
$i = $i * 2;
}
return $i * 48 == $fail_count;
};
}
my sub _handle_job_err {
my ($job, $err, $mail) = @_;
@ -121,18 +119,16 @@ my sub _handle_job_err {
$template_data->{"error"} = $err;
my $metadata_fields = {
type => "replication",
"job-id" => $job->{id},
# Hostname (without domain part)
hostname => PVE::INotify::nodename(),
type => "replication",
"job-id" => $job->{id},
# Hostname (without domain part)
hostname => PVE::INotify::nodename(),
};
eval {
PVE::Notify::error(
"replication",
$template_data,
$metadata_fields
);
PVE::Notify::error(
"replication", $template_data, $metadata_fields,
);
};
warn ": $@" if $@;
@ -145,22 +141,24 @@ sub run_jobs {
my $iteration = $now // time();
my $code = sub {
my $start_time = $now // time();
my $start_time = $now // time();
PVE::ReplicationState::purge_old_states();
PVE::ReplicationState::purge_old_states();
while (my $jobcfg = PVE::ReplicationState::get_next_job($iteration, $start_time)) {
my $guest_class = $lookup_guest_class->($jobcfg->{vmtype});
while (my $jobcfg = PVE::ReplicationState::get_next_job($iteration, $start_time)) {
my $guest_class = $lookup_guest_class->($jobcfg->{vmtype});
eval {
PVE::Replication::run_replication($guest_class, $jobcfg, $iteration, $start_time, $logfunc, $verbose);
};
if (my $err = $@) {
_handle_job_err($jobcfg, $err, $mail);
}
eval {
PVE::Replication::run_replication(
$guest_class, $jobcfg, $iteration, $start_time, $logfunc, $verbose,
);
};
if (my $err = $@) {
_handle_job_err($jobcfg, $err, $mail);
}
$start_time = $now // time();
}
$start_time = $now // time();
}
};
my $res = PVE::Tools::lock_file($pvesr_lock_path, 60, $code);
@ -177,215 +175,224 @@ my $extract_job_status = sub {
$data->{id} = $jobid;
foreach my $k (qw(last_sync last_try fail_count error duration)) {
$data->{$k} = $state->{$k} if defined($state->{$k});
$data->{$k} = $state->{$k} if defined($state->{$k});
}
if ($state->{pid} && $state->{ptime}) {
if (PVE::ProcFSTools::check_process_running($state->{pid}, $state->{ptime})) {
$data->{pid} = $state->{pid};
}
if (PVE::ProcFSTools::check_process_running($state->{pid}, $state->{ptime})) {
$data->{pid} = $state->{pid};
}
}
return $data;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'status',
path => '',
method => 'GET',
description => "List status of all replication jobs on this node.",
permissions => {
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
guest => get_standard_option('pve-vmid', {
optional => 1,
description => "Only list replication jobs for this guest.",
}),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
guest => get_standard_option(
'pve-vmid',
{
optional => 1,
description => "Only list replication jobs for this guest.",
},
),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $jobs = PVE::ReplicationState::job_status(1);
my $jobs = PVE::ReplicationState::job_status(1);
my $res = [];
foreach my $id (sort keys %$jobs) {
my $data = $extract_job_status->($jobs->{$id}, $id);
my $guest = $data->{guest};
next if defined($param->{guest}) && $guest != $param->{guest};
next if !$rpcenv->check($authuser, "/vms/$guest", [ 'VM.Audit' ]);
push @$res, $data;
}
my $res = [];
foreach my $id (sort keys %$jobs) {
my $data = $extract_job_status->($jobs->{$id}, $id);
my $guest = $data->{guest};
next if defined($param->{guest}) && $guest != $param->{guest};
next if !$rpcenv->check($authuser, "/vms/$guest", ['VM.Audit']);
push @$res, $data;
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '{id}',
method => 'GET',
permissions => { user => 'all' },
description => "Directory index.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return [
{ name => 'schedule_now' },
{ name => 'log' },
{ name => 'status' },
];
}});
return [
{ name => 'schedule_now' }, { name => 'log' }, { name => 'status' },
];
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'job_status',
path => '{id}/status',
method => 'GET',
description => "Get replication job status.",
permissions => {
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
},
returns => {
type => "object",
properties => {},
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $jobs = PVE::ReplicationState::job_status(1);
my $jobid = $param->{id};
my $jobcfg = $jobs->{$jobid};
my $jobs = PVE::ReplicationState::job_status(1);
my $jobid = $param->{id};
my $jobcfg = $jobs->{$jobid};
die "no such replication job '$jobid'\n" if !defined($jobcfg);
die "no such replication job '$jobid'\n" if !defined($jobcfg);
my $data = $extract_job_status->($jobcfg, $jobid);
my $guest = $data->{guest};
my $data = $extract_job_status->($jobcfg, $jobid);
my $guest = $data->{guest};
raise_perm_exc() if !$rpcenv->check($authuser, "/vms/$guest", [ 'VM.Audit' ]);
raise_perm_exc() if !$rpcenv->check($authuser, "/vms/$guest", ['VM.Audit']);
return $data;
}});
return $data;
},
});
__PACKAGE__->register_method({
name => 'read_job_log',
path => '{id}/log',
method => 'GET',
permissions => {
description => "Requires the VM.Audit permission on /vms/<vmid>, or 'Sys.Audit' on '/nodes/<node>'",
user => 'all',
description =>
"Requires the VM.Audit permission on /vms/<vmid>, or 'Sys.Audit' on '/nodes/<node>'",
user => 'all',
},
protected => 1,
description => "Read replication job log.",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
start => {
type => 'integer',
minimum => 0,
optional => 1,
},
limit => {
type => 'integer',
minimum => 0,
optional => 1,
},
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
start => {
type => 'integer',
minimum => 0,
optional => 1,
},
limit => {
type => 'integer',
minimum => 0,
optional => 1,
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
n => {
description=> "Line number",
type=> 'integer',
},
t => {
description=> "Line text",
type => 'string',
}
}
}
type => 'array',
items => {
type => "object",
properties => {
n => {
description => "Line number",
type => 'integer',
},
t => {
description => "Line text",
type => 'string',
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $jobid = $param->{id};
my $filename = PVE::ReplicationState::job_logfile_name($jobid);
my $jobid = $param->{id};
my $filename = PVE::ReplicationState::job_logfile_name($jobid);
my $cfg = PVE::ReplicationConfig->new();
my $data = $cfg->{ids}->{$jobid};
my $cfg = PVE::ReplicationConfig->new();
my $data = $cfg->{ids}->{$jobid};
die "no such replication job '$jobid'\n" if !defined($data);
die "no such replication job '$jobid'\n" if !defined($data);
my $node = $param->{node};
my $node = $param->{node};
my $vmid = $data->{guest};
raise_perm_exc() if (!($rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ]) ||
$rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Audit' ])));
my $vmid = $data->{guest};
raise_perm_exc()
if (!(
$rpcenv->check($authuser, "/vms/$vmid", ['VM.Audit'])
|| $rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'])
));
my ($count, $lines) = PVE::Tools::dump_logfile($filename, $param->{start}, $param->{limit});
my ($count, $lines) =
PVE::Tools::dump_logfile($filename, $param->{start}, $param->{limit});
$rpcenv->set_result_attrib('total', $count);
$rpcenv->set_result_attrib('total', $count);
return $lines;
}});
return $lines;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'schedule_now',
path => '{id}/schedule_now',
method => 'POST',
@ -393,30 +400,31 @@ __PACKAGE__->register_method ({
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/storage', ['Datastore.Allocate']],
check => ['perm', '/storage', ['Datastore.Allocate']],
},
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $jobid = $param->{id};
my $jobid = $param->{id};
my $cfg = PVE::ReplicationConfig->new();
my $jobcfg = $cfg->{ids}->{$jobid};
my $cfg = PVE::ReplicationConfig->new();
my $jobcfg = $cfg->{ids}->{$jobid};
die "no such replication job '$jobid'\n" if !defined($jobcfg);
die "no such replication job '$jobid'\n" if !defined($jobcfg);
PVE::ReplicationState::schedule_job_now($jobcfg);
PVE::ReplicationState::schedule_job_now($jobcfg);
}});
},
});
1;

View file

@ -14,268 +14,276 @@ use PVE::RESTHandler;
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
description => "List replication jobs.",
permissions => {
description => "Will only return replication jobs for which the calling user has"
. " VM.Audit permission on /vms/<vmid>.",
user => 'all',
description => "Will only return replication jobs for which the calling user has"
. " VM.Audit permission on /vms/<vmid>.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{id}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{id}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $cfg = PVE::ReplicationConfig->new();
my $cfg = PVE::ReplicationConfig->new();
my $res = [];
foreach my $id (sort keys %{$cfg->{ids}}) {
my $d = $cfg->{ids}->{$id};
my $vmid = $d->{guest};
next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
$d->{id} = $id;
push @$res, $d;
}
my $res = [];
foreach my $id (sort keys %{ $cfg->{ids} }) {
my $d = $cfg->{ids}->{$id};
my $vmid = $d->{guest};
next if !$rpcenv->check($authuser, "/vms/$vmid", ['VM.Audit'], 1);
$d->{id} = $id;
push @$res, $d;
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'read',
path => '{id}',
method => 'GET',
description => "Read replication job configuration.",
permissions => {
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
description => "Requires the VM.Audit permission on /vms/<vmid>.",
user => 'all',
},
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $cfg = PVE::ReplicationConfig->new();
my $cfg = PVE::ReplicationConfig->new();
my $data = $cfg->{ids}->{$param->{id}};
my $data = $cfg->{ids}->{ $param->{id} };
die "no such replication job '$param->{id}'\n" if !defined($data);
die "no such replication job '$param->{id}'\n" if !defined($data);
my $vmid = $data->{guest};
my $vmid = $data->{guest};
raise_perm_exc() if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ]);
raise_perm_exc() if !$rpcenv->check($authuser, "/vms/$vmid", ['VM.Audit']);
$data->{id} = $param->{id};
$data->{id} = $param->{id};
$data->{digest} = $cfg->{digest};
$data->{digest} = $cfg->{digest};
return $data;
}});
return $data;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
path => '',
protected => 1,
method => 'POST',
description => "Create a new replication job",
permissions => {
check => ['perm', '/storage', ['Datastore.Allocate']],
check => ['perm', '/storage', ['Datastore.Allocate']],
},
parameters => PVE::ReplicationConfig->createSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $type = extract_param($param, 'type');
my $plugin = PVE::ReplicationConfig->lookup($type);
my $id = extract_param($param, 'id');
my $type = extract_param($param, 'type');
my $plugin = PVE::ReplicationConfig->lookup($type);
my $id = extract_param($param, 'id');
# extract guest ID from job ID
my ($guest) = PVE::ReplicationConfig::parse_replication_job_id($id);
# extract guest ID from job ID
my ($guest) = PVE::ReplicationConfig::parse_replication_job_id($id);
my $nodelist = PVE::Cluster::get_members();
my $vmlist = PVE::Cluster::get_vmlist();
my $nodelist = PVE::Cluster::get_members();
my $vmlist = PVE::Cluster::get_vmlist();
my $guest_info = $vmlist->{ids}->{$guest};
my $guest_info = $vmlist->{ids}->{$guest};
die "Guest '$guest' does not exist.\n"
if !defined($guest_info);
die "Target '$param->{target}' does not exist.\n"
if !defined($nodelist->{$param->{target}});
die "Guest '$guest' does not exist.\n"
if !defined($guest_info);
die "Target '$param->{target}' does not exist.\n"
if !defined($nodelist->{ $param->{target} });
my $source = $guest_info->{node};
die "Source '$param->{source}' does not match current node of guest '$guest' ($source)\n"
if defined($param->{source}) && $param->{source} ne $source;
my $source = $guest_info->{node};
die
"Source '$param->{source}' does not match current node of guest '$guest' ($source)\n"
if defined($param->{source}) && $param->{source} ne $source;
$param->{source} //= $source;
$param->{source} //= $source;
die "Source and target must not be identical\n"
if $param->{target} eq $source;
die "Source and target must not be identical\n"
if $param->{target} eq $source;
my $guest_class = $PVE::API2::Replication::lookup_guest_class->($guest_info->{type});
my $guest_conf = $guest_class->load_config($guest, $source);
my $rep_volumes = $guest_class->get_replicatable_volumes(PVE::Storage::config(), $guest, $guest_conf, 0, 0);
die "No replicatable volumes found\n" if !%$rep_volumes;
my $guest_class = $PVE::API2::Replication::lookup_guest_class->($guest_info->{type});
my $guest_conf = $guest_class->load_config($guest, $source);
my $rep_volumes = $guest_class->get_replicatable_volumes(
PVE::Storage::config(), $guest, $guest_conf, 0, 0,
);
die "No replicatable volumes found\n" if !%$rep_volumes;
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
die "replication job '$id' already exists\n"
if $cfg->{ids}->{$id};
die "replication job '$id' already exists\n"
if $cfg->{ids}->{$id};
my $opts = $plugin->check_config($id, $param, 1, 1);
my $opts = $plugin->check_config($id, $param, 1, 1);
$opts->{guest} = $guest;
$opts->{guest} = $guest;
$cfg->{ids}->{$id} = $opts;
$cfg->{ids}->{$id} = $opts;
$cfg->write();
};
$cfg->write();
};
PVE::ReplicationConfig::lock($code);
PVE::ReplicationConfig::lock($code);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
protected => 1,
path => '{id}',
method => 'PUT',
description => "Update replication job configuration.",
permissions => {
check => ['perm', '/storage', ['Datastore.Allocate']],
check => ['perm', '/storage', ['Datastore.Allocate']],
},
parameters => PVE::ReplicationConfig->updateSchema(),
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $id = extract_param($param, 'id');
my $digest = extract_param($param, 'digest');
my $delete = extract_param($param, 'delete');
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
PVE::SectionConfig::assert_if_modified($cfg, $digest);
PVE::SectionConfig::assert_if_modified($cfg, $digest);
my $data = $cfg->{ids}->{$id};
die "no such job '$id'\n" if !$data;
my $data = $cfg->{ids}->{$id};
die "no such job '$id'\n" if !$data;
my $plugin = PVE::ReplicationConfig->lookup($data->{type});
my $opts = $plugin->check_config($id, $param, 0, 1);
my $plugin = PVE::ReplicationConfig->lookup($data->{type});
my $opts = $plugin->check_config($id, $param, 0, 1);
foreach my $k (keys %$opts) {
$data->{$k} = $opts->{$k};
}
foreach my $k (keys %$opts) {
$data->{$k} = $opts->{$k};
}
if ($delete) {
my $options = $plugin->private()->{options}->{$data->{type}};
foreach my $k (PVE::Tools::split_list($delete)) {
my $d = $options->{$k} ||
die "no such option '$k'\n";
die "unable to delete required option '$k'\n"
if !$d->{optional};
die "unable to delete fixed option '$k'\n"
if $d->{fixed};
delete $data->{$k};
}
}
if ($delete) {
my $options = $plugin->private()->{options}->{ $data->{type} };
foreach my $k (PVE::Tools::split_list($delete)) {
my $d = $options->{$k}
|| die "no such option '$k'\n";
die "unable to delete required option '$k'\n"
if !$d->{optional};
die "unable to delete fixed option '$k'\n"
if $d->{fixed};
delete $data->{$k};
}
}
$cfg->write();
};
$cfg->write();
};
PVE::ReplicationConfig::lock($code);
PVE::ReplicationConfig::lock($code);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
protected => 1,
path => '{id}',
method => 'DELETE',
description => "Mark replication job for removal.",
permissions => {
check => ['perm', '/storage', ['Datastore.Allocate']],
check => ['perm', '/storage', ['Datastore.Allocate']],
},
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
keep => {
description => "Keep replicated data at target (do not remove).",
type => 'boolean',
optional => 1,
default => 0,
},
force => {
description => "Will remove the jobconfig entry, but will not cleanup.",
type => 'boolean',
optional => 1,
default => 0,
},
}
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
keep => {
description => "Keep replicated data at target (do not remove).",
type => 'boolean',
optional => 1,
default => 0,
},
force => {
description => "Will remove the jobconfig entry, but will not cleanup.",
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
my $code = sub {
my $cfg = PVE::ReplicationConfig->new();
my $id = $param->{id};
if ($param->{force}) {
raise_param_exc({ 'keep' => "conflicts with parameter 'force'" }) if $param->{keep};
delete $cfg->{ids}->{$id};
} else {
my $jobcfg = $cfg->{ids}->{$id};
die "no such job '$id'\n" if !$jobcfg;
my $id = $param->{id};
if ($param->{force}) {
raise_param_exc({ 'keep' => "conflicts with parameter 'force'" })
if $param->{keep};
delete $cfg->{ids}->{$id};
} else {
my $jobcfg = $cfg->{ids}->{$id};
die "no such job '$id'\n" if !$jobcfg;
if (!$param->{keep} && $jobcfg->{type} eq 'local') {
# remove local snapshots and remote volumes
$jobcfg->{remove_job} = 'full';
} else {
# only remove local snapshots
$jobcfg->{remove_job} = 'local';
}
if (!$param->{keep} && $jobcfg->{type} eq 'local') {
# remove local snapshots and remote volumes
$jobcfg->{remove_job} = 'full';
} else {
# only remove local snapshots
$jobcfg->{remove_job} = 'local';
}
warn "Replication job removal is a background task and will take some time.\n"
if $rpcenv->{type} eq 'cli';
}
$cfg->write();
};
warn "Replication job removal is a background task and will take some time.\n"
if $rpcenv->{type} eq 'cli';
}
$cfg->write();
};
PVE::ReplicationConfig::lock($code);
PVE::ReplicationConfig::lock($code);
return undef;
}});
return undef;
},
});
1;

View file

@ -47,7 +47,7 @@ my $essential_services = {
# manage subinstances, of which the default is called "-".
# This is where we look for the daemon status
my $unit_extra_names = {
postfix => 'postfix@-'
postfix => 'postfix@-',
};
my $get_full_service_state = sub {
@ -56,10 +56,10 @@ my $get_full_service_state = sub {
my $res;
my $parser = sub {
my $line = shift;
if ($line =~ m/^([^=\s]+)=(.*)$/) {
$res->{$1} = $2;
}
my $line = shift;
if ($line =~ m/^([^=\s]+)=(.*)$/) {
$res->{$1} = $2;
}
};
PVE::Tools::run_command(['systemctl', 'show', $service], outfunc => $parser);
@ -75,11 +75,11 @@ sub get_service_list {
my $list = {};
foreach my $name (@$service_name_list) {
my $ss = eval { $get_full_service_state->($name) };
warn $@ if $@;
next if !$ss;
next if !defined($ss->{Description});
$list->{$name} = { name => $name, desc => $ss->{Description} };
my $ss = eval { $get_full_service_state->($name) };
warn $@ if $@;
next if !$ss;
next if !defined($ss->{Description});
$list->{$name} = { name => $name, desc => $ss->{Description} };
}
$static_service_list = $list;
@ -87,7 +87,6 @@ sub get_service_list {
return $static_service_list;
}
my $service_prop_desc = {
description => "Service ID",
type => 'string',
@ -99,10 +98,11 @@ my $service_cmd = sub {
my $initd_cmd;
die "unknown service command '$cmd'\n" if $cmd !~ m/^(start|stop|restart|reload|try-reload-or-restart)$/;
die "unknown service command '$cmd'\n"
if $cmd !~ m/^(start|stop|restart|reload|try-reload-or-restart)$/;
if ($essential_services->{$service} && $cmd eq 'stop') {
die "invalid service cmd '$service $cmd': refusing to stop essential service!\n";
die "invalid service cmd '$service $cmd': refusing to stop essential service!\n";
}
PVE::Tools::run_command(['systemctl', $cmd, $service]);
@ -115,67 +115,69 @@ my $service_state = sub {
my $ss = eval { $get_full_service_state->($service) };
if (my $err = $@) {
return $res;
return $res;
}
my $state = $ss->{SubState} || 'unknown';
if ($state eq 'dead' && $ss->{Type} && $ss->{Type} eq 'oneshot' && $ss->{Result}) {
$res->{state} = $ss->{Result};
$res->{state} = $ss->{Result};
} else {
$res->{state} = $ss->{SubState} || 'unknown';
$res->{state} = $ss->{SubState} || 'unknown';
}
if ($ss->{LoadState} eq 'not-found') {
$res->{'unit-state'} = 'not-found'; # not installed
$res->{'unit-state'} = 'not-found'; # not installed
} else {
$res->{'unit-state'} = $ss->{UnitFileState} || 'unknown';
$res->{'unit-state'} = $ss->{UnitFileState} || 'unknown';
}
$res->{'active-state'} = $ss->{ActiveState} || 'unknown';
return $res;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => '',
method => 'GET',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
check => ['perm', '/nodes/{node}', ['Sys.Audit']],
},
description => "Service list.",
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{service}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{service}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $service_list = get_service_list();
my $service_list = get_service_list();
my $res = [];
for my $id (sort keys %{$service_list}) {
my $state = $service_state->($id);
push @$res, {
service => $id,
name => $service_list->{$id}->{name},
desc => $service_list->{$id}->{desc},
%$state,
};
}
my $res = [];
for my $id (sort keys %{$service_list}) {
my $state = $service_state->($id);
push @$res,
{
service => $id,
name => $service_list->{$id}->{name},
desc => $service_list->{$id}->{desc},
%$state,
};
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method({
name => 'srvcmdidx',
@ -183,226 +185,232 @@ __PACKAGE__->register_method({
method => 'GET',
description => "Directory index",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
check => ['perm', '/nodes/{node}', ['Sys.Audit']],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [ { rel => 'child', href => "{subdir}" } ],
type => 'array',
items => {
type => "object",
properties => {
subdir => { type => 'string' },
},
},
links => [{ rel => 'child', href => "{subdir}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $res = [
{ subdir => 'state' },
{ subdir => 'start' },
{ subdir => 'stop' },
{ subdir => 'restart' },
{ subdir => 'reload' },
];
my $res = [
{ subdir => 'state' },
{ subdir => 'start' },
{ subdir => 'stop' },
{ subdir => 'restart' },
{ subdir => 'reload' },
];
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'service_state',
path => '{service}/state',
method => 'GET',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
check => ['perm', '/nodes/{node}', ['Sys.Audit']],
},
description => "Read service properties",
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => "object",
properties => {},
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $id = $param->{service};
my $id = $param->{service};
my $service_list = get_service_list();
my $service_list = get_service_list();
my $si = $service_list->{$id};
my $si = $service_list->{$id};
my $state = $service_state->($id);
my $state = $service_state->($id);
return {
service => $param->{service},
name => $si->{name},
desc => $si->{desc},
%$state,
};
}});
return {
service => $param->{service},
name => $si->{name},
desc => $si->{desc},
%$state,
};
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'service_start',
path => '{service}/start',
method => 'POST',
description => "Start service.",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $user = $rpcenv->get_user();
my $realcmd = sub {
my $upid = shift;
my $realcmd = sub {
my $upid = shift;
syslog('info', "starting service $param->{service}: $upid\n");
syslog('info', "starting service $param->{service}: $upid\n");
$service_cmd->($param->{service}, 'start');
$service_cmd->($param->{service}, 'start');
};
};
return $rpcenv->fork_worker('srvstart', $param->{service}, $user, $realcmd);
}});
return $rpcenv->fork_worker('srvstart', $param->{service}, $user, $realcmd);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'service_stop',
path => '{service}/stop',
method => 'POST',
description => "Stop service.",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $user = $rpcenv->get_user();
my $realcmd = sub {
my $upid = shift;
my $realcmd = sub {
my $upid = shift;
syslog('info', "stopping service $param->{service}: $upid\n");
syslog('info', "stopping service $param->{service}: $upid\n");
$service_cmd->($param->{service}, 'stop');
$service_cmd->($param->{service}, 'stop');
};
};
return $rpcenv->fork_worker('srvstop', $param->{service}, $user, $realcmd);
}});
return $rpcenv->fork_worker('srvstop', $param->{service}, $user, $realcmd);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'service_restart',
path => '{service}/restart',
method => 'POST',
description => "Hard restart service. Use reload if you want to reduce interruptions.",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $realcmd = sub {
my $upid = shift;
syslog('info', "re-starting service $param->{service}: $upid\n");
my $realcmd = sub {
my $upid = shift;
syslog('info', "re-starting service $param->{service}: $upid\n");
$service_cmd->($param->{service}, 'restart');
};
$service_cmd->($param->{service}, 'restart');
};
return $rpcenv->fork_worker('srvrestart', $param->{service}, $user, $realcmd);
}});
return $rpcenv->fork_worker('srvrestart', $param->{service}, $user, $realcmd);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'service_reload',
path => '{service}/reload',
method => 'POST',
description => "Reload service. Falls back to restart if service cannot be reloaded.",
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
service => $service_prop_desc,
},
},
returns => {
type => 'string',
type => 'string',
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $realcmd = sub {
my $upid = shift;
syslog('info', "reloading service $param->{service}: $upid\n");
my $realcmd = sub {
my $upid = shift;
syslog('info', "reloading service $param->{service}: $upid\n");
$service_cmd->($param->{service}, 'try-reload-or-restart');
$service_cmd->($param->{service}, 'try-reload-or-restart');
};
};
return $rpcenv->fork_worker('srvreload', $param->{service}, $user, $realcmd);
}});
return $rpcenv->fork_worker('srvreload', $param->{service}, $user, $realcmd);
},
});

View file

@ -39,7 +39,7 @@ sub parse_key {
my ($key, $noerr) = @_;
if ($key =~ m/^${subscription_pattern}$/) {
return wantarray ? ($1, $2) : $1; # number of sockets, level
return wantarray ? ($1, $2) : $1; # number of sockets, level
}
return undef if $noerr;
@ -51,7 +51,7 @@ sub check_key {
my ($sockets, $level) = parse_key($key);
if ($sockets < $req_sockets) {
die "wrong number of sockets ($sockets < $req_sockets)\n";
die "wrong number of sockets ($sockets < $req_sockets)\n";
}
return ($sockets, $level);
}
@ -67,11 +67,11 @@ sub read_etc_subscription {
my ($sockets, $level);
eval { ($sockets, $level) = check_key($info->{key}, $req_sockets); };
if (my $err = $@) {
chomp $err;
$info->{status} = 'invalid';
$info->{message} = $err;
chomp $err;
$info->{status} = 'invalid';
$info->{message} = $err;
} else {
$info->{level} = $level;
$info->{level} = $level;
}
return $info;
@ -83,7 +83,7 @@ my sub cache_is_valid {
return if !$info || $info->{status} ne 'active';
my $checked_info = Proxmox::RS::Subscription::check_age($info, 1);
return $checked_info->{status} eq 'active'
return $checked_info->{status} eq 'active';
}
sub write_etc_subscription {
@ -92,25 +92,32 @@ sub write_etc_subscription {
my $server_id = PVE::API2Tools::get_hwaddress();
mkdir "/etc/apt/auth.conf.d";
Proxmox::RS::Subscription::write_subscription(
$filename, "/etc/apt/auth.conf.d/pve.conf", "enterprise.proxmox.com/debian/pve", $info);
$filename,
"/etc/apt/auth.conf.d/pve.conf",
"enterprise.proxmox.com/debian/pve",
$info,
);
if (!(defined($info->{key}) && defined($info->{serverid}))) {
unlink "/etc/apt/auth.conf.d/ceph.conf" or $!{ENOENT} or die "failed to remove apt auth ceph.conf - $!";
unlink "/etc/apt/auth.conf.d/ceph.conf"
or $!{ENOENT}
or die "failed to remove apt auth ceph.conf - $!";
} else {
my $supported_ceph_releases = PVE::Ceph::Releases::get_available_ceph_release_codenames(1);
my $ceph_auth = '';
for my $ceph_release ($supported_ceph_releases->@*) {
$ceph_auth .= "machine enterprise.proxmox.com/debian/ceph-${ceph_release}"
." login $info->{key} password $info->{serverid}\n"
}
# add a generic one to handle the case where a new ceph release was made available while
# the subscription info was not yet updated, and as per APT_AUTH.CONF(5) start-with matches.
$ceph_auth .= "machine enterprise.proxmox.com/debian/ceph login $info->{key} password $info->{serverid}\n";
PVE::Tools::file_set_contents("/etc/apt/auth.conf.d/ceph.conf", $ceph_auth, 0600);
my $supported_ceph_releases = PVE::Ceph::Releases::get_available_ceph_release_codenames(1);
my $ceph_auth = '';
for my $ceph_release ($supported_ceph_releases->@*) {
$ceph_auth .= "machine enterprise.proxmox.com/debian/ceph-${ceph_release}"
. " login $info->{key} password $info->{serverid}\n";
}
# add a generic one to handle the case where a new ceph release was made available while
# the subscription info was not yet updated, and as per APT_AUTH.CONF(5) start-with matches.
$ceph_auth .=
"machine enterprise.proxmox.com/debian/ceph login $info->{key} password $info->{serverid}\n";
PVE::Tools::file_set_contents("/etc/apt/auth.conf.d/ceph.conf", $ceph_auth, 0600);
}
}
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get',
path => '',
method => 'GET',
@ -118,239 +125,247 @@ __PACKAGE__->register_method ({
proxyto => 'node',
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'object',
additionalProperties => 0,
properties => {
status => {
type => 'string',
enum => [qw(new notfound active invalid expired suspended)],
description => "The current subscription status.",
},
checktime => {
type => 'integer',
description => 'Timestamp of the last check done.',
optional => 1,
},
key => {
type => 'string',
description => 'The subscription key, if set and permitted to access.',
optional => 1,
},
level => {
type => 'string',
description => 'A short code for the subscription level.',
optional => 1,
},
message => {
type => 'string',
description => 'A more human readable status message.',
optional => 1,
},
nextduedate => {
type => 'string',
description => 'Next due date of the set subscription.',
optional => 1,
},
productname => {
type => 'string',
description => 'Human readable productname of the set subscription.',
optional => 1,
},
regdate => {
type => 'string',
description => 'Register date of the set subscription.',
optional => 1,
},
serverid => {
type => 'string',
description => 'The server ID, if permitted to access.',
optional => 1,
},
signature => {
type => 'string',
description => 'Signature for offline keys',
optional => 1,
},
sockets => {
type => 'integer',
description => 'The number of sockets for this host.',
optional => 1,
},
url => {
type => 'string',
description => 'URL to the web shop.',
optional => 1,
},
},
type => 'object',
additionalProperties => 0,
properties => {
status => {
type => 'string',
enum => [qw(new notfound active invalid expired suspended)],
description => "The current subscription status.",
},
checktime => {
type => 'integer',
description => 'Timestamp of the last check done.',
optional => 1,
},
key => {
type => 'string',
description => 'The subscription key, if set and permitted to access.',
optional => 1,
},
level => {
type => 'string',
description => 'A short code for the subscription level.',
optional => 1,
},
message => {
type => 'string',
description => 'A more human readable status message.',
optional => 1,
},
nextduedate => {
type => 'string',
description => 'Next due date of the set subscription.',
optional => 1,
},
productname => {
type => 'string',
description => 'Human readable productname of the set subscription.',
optional => 1,
},
regdate => {
type => 'string',
description => 'Register date of the set subscription.',
optional => 1,
},
serverid => {
type => 'string',
description => 'The server ID, if permitted to access.',
optional => 1,
},
signature => {
type => 'string',
description => 'Signature for offline keys',
optional => 1,
},
sockets => {
type => 'integer',
description => 'The number of sockets for this host.',
optional => 1,
},
url => {
type => 'string',
description => 'URL to the web shop.',
optional => 1,
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = $param->{node};
my $node = $param->{node};
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $has_permission = $rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'], 1);
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $has_permission = $rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'], 1);
my $server_id = PVE::API2Tools::get_hwaddress();
my $url = "https://www.proxmox.com/en/proxmox-virtual-environment/pricing";
my $server_id = PVE::API2Tools::get_hwaddress();
my $url = "https://www.proxmox.com/en/proxmox-virtual-environment/pricing";
my $info = read_etc_subscription();
if (!$info) {
my $no_subscription_info = {
status => "notfound",
message => "There is no subscription key",
url => $url,
};
$no_subscription_info->{serverid} = $server_id if $has_permission;
return $no_subscription_info;
}
my $info = read_etc_subscription();
if (!$info) {
my $no_subscription_info = {
status => "notfound",
message => "There is no subscription key",
url => $url,
};
$no_subscription_info->{serverid} = $server_id if $has_permission;
return $no_subscription_info;
}
if (!$has_permission) {
return {
status => $info->{status},
message => $info->{message},
url => $url,
}
}
if (!$has_permission) {
return {
status => $info->{status},
message => $info->{message},
url => $url,
};
}
$info->{serverid} = $server_id;
$info->{sockets} = get_sockets();
$info->{url} = $url;
$info->{serverid} = $server_id;
$info->{sockets} = get_sockets();
$info->{url} = $url;
return $info
}});
return $info;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
path => '',
method => 'POST',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Update subscription info.",
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
description => "Always connect to server, even if local cache is still valid.",
type => 'boolean',
optional => 1,
default => 0
}
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
force => {
description => "Always connect to server, even if local cache is still valid.",
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'null'},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $info = read_etc_subscription();
return undef if !$info;
my $info = read_etc_subscription();
return undef if !$info;
my $server_id = PVE::API2Tools::get_hwaddress();
my $key = $info->{key};
my $server_id = PVE::API2Tools::get_hwaddress();
my $key = $info->{key};
die "Updating offline key not possible - please remove and re-add subscription key to switch to online key.\n"
if $info->{signature};
die
"Updating offline key not possible - please remove and re-add subscription key to switch to online key.\n"
if $info->{signature};
return undef if !$param->{force} && cache_is_valid($info); # key has been recently checked
return undef if !$param->{force} && cache_is_valid($info); # key has been recently checked
my $req_sockets = get_sockets();
check_key($key, $req_sockets);
my $req_sockets = get_sockets();
check_key($key, $req_sockets);
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $proxy = $dccfg->{http_proxy};
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $proxy = $dccfg->{http_proxy};
$info = Proxmox::RS::Subscription::check_subscription($key, $server_id, "", "Proxmox VE", $proxy);
$info = Proxmox::RS::Subscription::check_subscription(
$key, $server_id, "", "Proxmox VE", $proxy,
);
write_etc_subscription($info);
write_etc_subscription($info);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'set',
path => '',
method => 'PUT',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Set subscription key.",
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
key => {
description => "Proxmox VE subscription key",
type => 'string',
pattern => "\\s*${subscription_pattern}\\s*",
maxLength => 32,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
key => {
description => "Proxmox VE subscription key",
type => 'string',
pattern => "\\s*${subscription_pattern}\\s*",
maxLength => 32,
},
},
},
returns => { type => 'null'},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $key = PVE::Tools::trim($param->{key});
my $key = PVE::Tools::trim($param->{key});
my $new_info = {
status => 'New',
key => $key,
checktime => time(),
};
my $new_info = {
status => 'New',
key => $key,
checktime => time(),
};
my $req_sockets = get_sockets();
my $server_id = PVE::API2Tools::get_hwaddress();
my $req_sockets = get_sockets();
my $server_id = PVE::API2Tools::get_hwaddress();
check_key($key, $req_sockets);
check_key($key, $req_sockets);
write_etc_subscription($new_info);
write_etc_subscription($new_info);
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $proxy = $dccfg->{http_proxy};
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $proxy = $dccfg->{http_proxy};
my $checked_info = Proxmox::RS::Subscription::check_subscription(
$key, $server_id, "", "Proxmox VE", $proxy);
my $checked_info = Proxmox::RS::Subscription::check_subscription(
$key, $server_id, "", "Proxmox VE", $proxy,
);
write_etc_subscription($checked_info);
write_etc_subscription($checked_info);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
path => '',
method => 'DELETE',
permissions => {
check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
check => ['perm', '/nodes/{node}', ['Sys.Modify']],
},
description => "Delete subscription key of this node.",
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => { type => 'null'},
returns => { type => 'null' },
code => sub {
my $subscription_file = '/etc/subscription';
return if ! -e $subscription_file;
unlink($subscription_file) or die "cannot delete subscription key: $!";
return undef;
}});
my $subscription_file = '/etc/subscription';
return if !-e $subscription_file;
unlink($subscription_file) or die "cannot delete subscription key: $!";
return undef;
},
});
1;

View file

@ -20,7 +20,7 @@ my $convert_token_task = sub {
my ($task) = @_;
if (PVE::AccessControl::pve_verify_tokenid($task->{user}, 1)) {
($task->{user}, $task->{tokenid}) = PVE::AccessControl::split_tokenid($task->{user});
($task->{user}, $task->{tokenid}) = PVE::AccessControl::split_tokenid($task->{user});
}
};
@ -28,11 +28,11 @@ my $check_task_user = sub {
my ($task, $user) = @_;
if ($task->{tokenid}) {
my $fulltoken = PVE::AccessControl::join_tokenid($task->{user}, $task->{tokenid});
# token only sees token tasks, user sees user + token tasks
return $user eq $fulltoken || $user eq $task->{user};
my $fulltoken = PVE::AccessControl::join_tokenid($task->{user}, $task->{tokenid});
# token only sees token tasks, user sees user + token tasks
return $user eq $fulltoken || $user eq $task->{user};
} else {
return $user eq $task->{user};
return $user eq $task->{user};
}
};
@ -41,211 +41,216 @@ __PACKAGE__->register_method({
path => '',
method => 'GET',
permissions => {
description => "List task associated with the current user, or all task the user has 'Sys.Audit' permissions on /nodes/<node> (the <node> the task runs on).",
user => 'all'
description =>
"List task associated with the current user, or all task the user has 'Sys.Audit' permissions on /nodes/<node> (the <node> the task runs on).",
user => 'all',
},
description => "Read task list for one node (finished tasks).",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "List tasks beginning from this offset.",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "Only list this amount of tasks.",
},
userfilter => {
type => 'string',
optional => 1,
description => "Only list tasks from this user.",
},
typefilter => {
type => 'string',
optional => 1,
description => 'Only list tasks of this type (e.g., vzstart, vzdump).',
},
vmid => get_standard_option('pve-vmid', {
description => "Only list tasks for this VM.",
optional => 1,
}),
errors => {
type => 'boolean',
default => 0,
optional => 1,
description => 'Only list tasks with a status of ERROR.',
},
source => {
type => 'string',
enum => ['archive', 'active', 'all'],
default => 'archive',
optional => 1,
description => 'List archived, active or all tasks.',
},
since => {
type => 'integer',
description => "Only list tasks since this UNIX epoch.",
optional => 1,
},
until => {
type => 'integer',
description => "Only list tasks until this UNIX epoch.",
optional => 1,
},
statusfilter => {
type => 'string',
format => 'pve-task-status-type-list',
optional => 1,
description => 'List of Task States that should be returned.',
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "List tasks beginning from this offset.",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "Only list this amount of tasks.",
},
userfilter => {
type => 'string',
optional => 1,
description => "Only list tasks from this user.",
},
typefilter => {
type => 'string',
optional => 1,
description => 'Only list tasks of this type (e.g., vzstart, vzdump).',
},
vmid => get_standard_option(
'pve-vmid',
{
description => "Only list tasks for this VM.",
optional => 1,
},
),
errors => {
type => 'boolean',
default => 0,
optional => 1,
description => 'Only list tasks with a status of ERROR.',
},
source => {
type => 'string',
enum => ['archive', 'active', 'all'],
default => 'archive',
optional => 1,
description => 'List archived, active or all tasks.',
},
since => {
type => 'integer',
description => "Only list tasks since this UNIX epoch.",
optional => 1,
},
until => {
type => 'integer',
description => "Only list tasks until this UNIX epoch.",
optional => 1,
},
statusfilter => {
type => 'string',
format => 'pve-task-status-type-list',
optional => 1,
description => 'List of Task States that should be returned.',
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
upid => { type => 'string', title => 'UPID', },
node => { type => 'string', title => 'Node', },
pid => { type => 'integer', title => 'PID', },
pstart => { type => 'integer', },
starttime => { type => 'integer', title => 'Starttime', },
type => { type => 'string', title => 'Type', },
id => { type => 'string', title => 'ID', },
user => { type => 'string', title => 'User', },
endtime => { type => 'integer', optional => 1, title => 'Endtime', },
status => { type => 'string', optional => 1, title => 'Status', },
},
},
links => [ { rel => 'child', href => "{upid}" } ],
type => 'array',
items => {
type => "object",
properties => {
upid => { type => 'string', title => 'UPID' },
node => { type => 'string', title => 'Node' },
pid => { type => 'integer', title => 'PID' },
pstart => { type => 'integer' },
starttime => { type => 'integer', title => 'Starttime' },
type => { type => 'string', title => 'Type' },
id => { type => 'string', title => 'ID' },
user => { type => 'string', title => 'User' },
endtime => { type => 'integer', optional => 1, title => 'Endtime' },
status => { type => 'string', optional => 1, title => 'Status' },
},
},
links => [{ rel => 'child', href => "{upid}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $res = [];
my $res = [];
my $filename = "/var/log/pve/tasks/index";
my $filename = "/var/log/pve/tasks/index";
my $node = $param->{node};
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
my $userfilter = $param->{userfilter};
my $typefilter = $param->{typefilter};
my $errors = $param->{errors} // 0;
my $source = $param->{source} // 'archive';
my $since = $param->{since};
my $until = $param->{until};
my $statusfilter = {
ok => 1,
warning => 1,
error => 1,
unknown => 1,
};
my $node = $param->{node};
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
my $userfilter = $param->{userfilter};
my $typefilter = $param->{typefilter};
my $errors = $param->{errors} // 0;
my $source = $param->{source} // 'archive';
my $since = $param->{since};
my $until = $param->{until};
my $statusfilter = {
ok => 1,
warning => 1,
error => 1,
unknown => 1,
};
if (defined($param->{statusfilter}) && !$errors) {
$statusfilter = {
ok => 0,
warning => 0,
error => 0,
unknown => 0,
};
for my $filter (PVE::Tools::split_list($param->{statusfilter})) {
$statusfilter->{lc($filter)} = 1 ;
}
} elsif ($errors) {
$statusfilter->{ok} = 0;
}
if (defined($param->{statusfilter}) && !$errors) {
$statusfilter = {
ok => 0,
warning => 0,
error => 0,
unknown => 0,
};
for my $filter (PVE::Tools::split_list($param->{statusfilter})) {
$statusfilter->{ lc($filter) } = 1;
}
} elsif ($errors) {
$statusfilter->{ok} = 0;
}
my $count = 0;
my $line;
my $count = 0;
my $line;
my $auditor = $rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ], 1);
my $auditor = $rpcenv->check($user, "/nodes/$node", ['Sys.Audit'], 1);
my $filter_task = sub {
my $task = shift;
my $filter_task = sub {
my $task = shift;
return 1 if $userfilter && $task->{user} !~ m/\Q$userfilter\E/i;
return 1 if !($auditor || $check_task_user->($task, $user));
return 1 if $userfilter && $task->{user} !~ m/\Q$userfilter\E/i;
return 1 if !($auditor || $check_task_user->($task, $user));
return 1 if $typefilter && $task->{type} ne $typefilter;
return 1 if $typefilter && $task->{type} ne $typefilter;
return 1 if $param->{vmid} && (!$task->{id} || $task->{id} ne $param->{vmid});
return 1 if $param->{vmid} && (!$task->{id} || $task->{id} ne $param->{vmid});
return 1 if defined($since) && $task->{starttime} < $since;
return 1 if defined($until) && $task->{starttime} > $until;
return 1 if defined($since) && $task->{starttime} < $since;
return 1 if defined($until) && $task->{starttime} > $until;
my $type = PVE::Tools::upid_normalize_status_type($task->{status});
return 1 if !$statusfilter->{$type};
my $type = PVE::Tools::upid_normalize_status_type($task->{status});
return 1 if !$statusfilter->{$type};
return 1 if $count++ < $start;
return 1 if $limit <= 0;
return 1 if $count++ < $start;
return 1 if $limit <= 0;
return 0;
};
return 0;
};
my $parse_line = sub {
if ($line =~ m/^(\S+)(\s([0-9A-Za-z]{8})(\s(\S.*))?)?$/) {
my $upid = $1;
my $endtime = $3;
my $status = $5;
if ((my $task = PVE::Tools::upid_decode($upid, 1))) {
my $parse_line = sub {
if ($line =~ m/^(\S+)(\s([0-9A-Za-z]{8})(\s(\S.*))?)?$/) {
my $upid = $1;
my $endtime = $3;
my $status = $5;
if ((my $task = PVE::Tools::upid_decode($upid, 1))) {
$task->{upid} = $upid;
$task->{endtime} = hex($endtime) if $endtime;
$task->{status} = $status if $status;
$task->{upid} = $upid;
$task->{endtime} = hex($endtime) if $endtime;
$task->{status} = $status if $status;
$convert_token_task->($task);
if (!$filter_task->($task)) {
push @$res, $task;
$limit--;
}
}
}
};
$convert_token_task->($task);
if (!$filter_task->($task)) {
push @$res, $task;
$limit--;
}
}
}
};
if ($source eq 'active' || $source eq 'all') {
my $recent_tasks = PVE::INotify::read_file('active');
for my $task (@$recent_tasks) {
next if $task->{saved}; # archived task, already in index(.1)
if (!$filter_task->($task)) {
$task->{status} = 'RUNNING' if !$task->{status}; # otherwise it would be archived
push @$res, $task;
$limit--;
}
}
}
if ($source eq 'active' || $source eq 'all') {
my $recent_tasks = PVE::INotify::read_file('active');
for my $task (@$recent_tasks) {
next if $task->{saved}; # archived task, already in index(.1)
if (!$filter_task->($task)) {
$task->{status} = 'RUNNING' if !$task->{status}; # otherwise it would be archived
push @$res, $task;
$limit--;
}
}
}
if ($source ne 'active') {
if (my $bw = File::ReadBackwards->new($filename)) {
while (defined ($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
if (my $bw = File::ReadBackwards->new("$filename.1")) {
while (defined ($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
}
if ($source ne 'active') {
if (my $bw = File::ReadBackwards->new($filename)) {
while (defined($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
if (my $bw = File::ReadBackwards->new("$filename.1")) {
while (defined($line = $bw->readline)) {
&$parse_line();
}
$bw->close();
}
}
$rpcenv->set_result_attrib('total', $count);
$rpcenv->set_result_attrib('total', $count);
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method({
name => 'upid_index',
@ -254,28 +259,28 @@ __PACKAGE__->register_method({
description => '', # index helper
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
}
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
type => 'array',
items => {
type => "object",
properties => {},
},
links => [{ rel => 'child', href => "{name}" }],
},
code => sub {
my ($param) = @_;
my ($param) = @_;
return [
{ name => 'log' },
{ name => 'status' }
];
}});
return [
{ name => 'log' }, { name => 'status' },
];
},
});
__PACKAGE__->register_method({
name => 'stop_task',
@ -283,153 +288,159 @@ __PACKAGE__->register_method({
method => 'DELETE',
description => 'Stop a task.',
permissions => {
description => "The user needs 'Sys.Modify' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
description =>
"The user needs 'Sys.Modify' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
}
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => { type => 'string' },
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if ! -f $filename;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if !-f $filename;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Modify' ]);
}
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", ['Sys.Modify']);
}
PVE::RPCEnvironment->check_worker($param->{upid}, 1);
PVE::RPCEnvironment->check_worker($param->{upid}, 1);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method({
name => 'read_task_log',
path => '{upid}/log',
method => 'GET',
permissions => {
description => "The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
description =>
"The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they aren't the owner of the task.",
user => 'all',
},
protected => 1,
download_allowed => 1,
description => "Read task log.",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "Start at this line when reading the tasklog",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "The amount of lines to read from the tasklog.",
},
download => {
type => 'boolean',
optional => 1,
description => "Whether the tasklog file should be downloaded. This parameter can't be used in conjunction with other parameters",
}
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
start => {
type => 'integer',
minimum => 0,
default => 0,
optional => 1,
description => "Start at this line when reading the tasklog",
},
limit => {
type => 'integer',
minimum => 0,
default => 50,
optional => 1,
description => "The amount of lines to read from the tasklog.",
},
download => {
type => 'boolean',
optional => 1,
description =>
"Whether the tasklog file should be downloaded. This parameter can't be used in conjunction with other parameters",
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
n => {
description=> "Line number",
type=> 'integer',
},
t => {
description=> "Line text",
type => 'string',
}
}
}
type => 'array',
items => {
type => "object",
properties => {
n => {
description => "Line number",
type => 'integer',
},
t => {
description => "Line text",
type => 'string',
},
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
}
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", ['Sys.Audit']);
}
if ($param->{download}) {
if (defined($param->{start}) || defined($param->{limit})) {
die "'download' cannot be used together with 'start' or 'limit' parameters\n";
}
# 1024 is a practical cutoff for the size distribution of our log files.
my $use_compression = ( -s $filename ) > 1024;
if ($param->{download}) {
if (defined($param->{start}) || defined($param->{limit})) {
die "'download' cannot be used together with 'start' or 'limit' parameters\n";
}
# 1024 is a practical cutoff for the size distribution of our log files.
my $use_compression = (-s $filename) > 1024;
my $fh;
if ($use_compression) {
open($fh, "-|", "/usr/bin/gzip", "-c", "$filename")
or die "Could not create compressed file stream for file '$filename' - $!\n";
} else {
open($fh, '<', $filename) or die "Could not open file '$filename' - $!\n";
}
my $fh;
if ($use_compression) {
open($fh, "-|", "/usr/bin/gzip", "-c", "$filename")
or die "Could not create compressed file stream for file '$filename' - $!\n";
} else {
open($fh, '<', $filename) or die "Could not open file '$filename' - $!\n";
}
my $task_time = strftime('%FT%TZ', gmtime($task->{starttime}));
my $download_name = 'task-'.$task->{node}.'-'.$task->{type}.'-'.$task_time.'.log';
my $task_time = strftime('%FT%TZ', gmtime($task->{starttime}));
my $download_name =
'task-' . $task->{node} . '-' . $task->{type} . '-' . $task_time . '.log';
return {
download => {
fh => $fh,
stream => 1,
'content-encoding' => $use_compression ? 'gzip' : undef,
'content-type' => "text/plain",
'content-disposition' => "attachment; filename=\"".$download_name."\"",
},
},
} else {
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
return {
download => {
fh => $fh,
stream => 1,
'content-encoding' => $use_compression ? 'gzip' : undef,
'content-type' => "text/plain",
'content-disposition' => "attachment; filename=\"" . $download_name . "\"",
},
},
;
} else {
my $start = $param->{start} // 0;
my $limit = $param->{limit} // 50;
my ($count, $lines) = PVE::Tools::dump_logfile($filename, $start, $limit);
my ($count, $lines) = PVE::Tools::dump_logfile($filename, $start, $limit);
$rpcenv->set_result_attrib('total', $count);
return $lines;
}
}});
$rpcenv->set_result_attrib('total', $count);
return $lines;
}
},
});
my $exit_status_cache = {};
@ -438,92 +449,94 @@ __PACKAGE__->register_method({
path => '{upid}/status',
method => 'GET',
permissions => {
description => "The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they are not the owner of the task.",
user => 'all',
description =>
"The user needs 'Sys.Audit' permissions on '/nodes/<node>' if they are not the owner of the task.",
user => 'all',
},
protected => 1,
description => "Read task status.",
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
upid => {
type => 'string',
description => "The task's unique ID.",
},
},
},
returns => {
type => "object",
properties => {
pid => {
type => 'integer'
},
status => {
type => 'string', enum => ['running', 'stopped'],
},
type => {
type => 'string',
},
id => {
type => 'string',
},
user => {
type => 'string',
},
exitstatus => {
type => 'string',
optional => 1,
},
upid => {
type => 'string',
},
starttime => {
type => 'integer',
},
pstart => {
type => 'integer',
},
node => {
type => 'string',
},
},
type => "object",
properties => {
pid => {
type => 'integer',
},
status => {
type => 'string',
enum => ['running', 'stopped'],
},
type => {
type => 'string',
},
id => {
type => 'string',
},
user => {
type => 'string',
},
exitstatus => {
type => 'string',
optional => 1,
},
upid => {
type => 'string',
},
starttime => {
type => 'integer',
},
pstart => {
type => 'integer',
},
node => {
type => 'string',
},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if ! -f $filename;
my ($task, $filename) = PVE::Tools::upid_decode($param->{upid}, 1);
raise_param_exc({ upid => "unable to parse worker upid" }) if !$task;
raise_param_exc({ upid => "no such task" }) if !-f $filename;
my $lines = [];
my $lines = [];
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $node = $param->{node};
$convert_token_task->($task);
$convert_token_task->($task);
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", [ 'Sys.Audit' ]);
}
if (!$check_task_user->($task, $user)) {
$rpcenv->check($user, "/nodes/$node", ['Sys.Audit']);
}
my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
$task->{status} = ($pstart && ($pstart == $task->{pstart})) ?
'running' : 'stopped';
my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
$task->{status} = ($pstart && ($pstart == $task->{pstart})) ? 'running' : 'stopped';
$task->{upid} = $param->{upid}; # include upid
$task->{upid} = $param->{upid}; # include upid
if ($task->{status} eq 'stopped') {
if (!defined($exit_status_cache->{$task->{upid}})) {
$exit_status_cache->{$task->{upid}} =
PVE::Tools::upid_read_status($task->{upid});
}
$task->{exitstatus} = $exit_status_cache->{$task->{upid}};
}
if ($task->{status} eq 'stopped') {
if (!defined($exit_status_cache->{ $task->{upid} })) {
$exit_status_cache->{ $task->{upid} } =
PVE::Tools::upid_read_status($task->{upid});
}
$task->{exitstatus} = $exit_status_cache->{ $task->{upid} };
}
return $task;
}});
return $task;
},
});
1;

View file

@ -28,282 +28,287 @@ my sub assert_param_permission_vzdump {
PVE::API2::Backup::assert_param_permission_common($rpcenv, $user, $param);
if (defined($param->{maxfiles}) || defined($param->{'prune-backups'})) {
if (my $storeid = PVE::VZDump::get_storage_param($param)) {
$rpcenv->check($user, "/storage/$storeid", [ 'Datastore.Allocate' ]);
}
if (my $storeid = PVE::VZDump::get_storage_param($param)) {
$rpcenv->check($user, "/storage/$storeid", ['Datastore.Allocate']);
}
}
}
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'vzdump',
path => '',
method => 'POST',
description => "Create backup.",
permissions => {
description => "The user needs 'VM.Backup' permissions on any VM, and "
."'Datastore.AllocateSpace' on the backup storage (and fleecing storage when fleecing "
."is used). The 'tmpdir', 'dumpdir', 'script' and 'job-id' 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',
description => "The user needs 'VM.Backup' permissions on any VM, and "
. "'Datastore.AllocateSpace' on the backup storage (and fleecing storage when fleecing "
. "is used). The 'tmpdir', 'dumpdir', 'script' and 'job-id' 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,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => PVE::VZDump::Common::json_config_properties({
'job-id' => get_standard_option('pve-backup-jobid', {
description => "The ID of the backup job. If set, the 'backup-job' metadata field"
. " of the backup notification will be set to this value. Only root\@pam"
. " can set this parameter.",
optional => 1,
}),
stdout => {
type => 'boolean',
description => "Write tar to stdout, not to a file.",
optional => 1,
},
additionalProperties => 0,
properties => PVE::VZDump::Common::json_config_properties({
'job-id' => get_standard_option(
'pve-backup-jobid',
{
description =>
"The ID of the backup job. If set, the 'backup-job' metadata field"
. " of the backup notification will be set to this value. Only root\@pam"
. " can set this parameter.",
optional => 1,
},
),
stdout => {
type => 'boolean',
description => "Write tar to stdout, not to a file.",
optional => 1,
},
}),
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $user = $rpcenv->get_user();
my $nodename = PVE::INotify::nodename();
my $nodename = PVE::INotify::nodename();
if ($rpcenv->{type} ne 'cli') {
raise_param_exc({ node => "option is only allowed on the command line interface."})
if $param->{node} && $param->{node} ne $nodename;
if ($rpcenv->{type} ne 'cli') {
raise_param_exc({ node => "option is only allowed on the command line interface." })
if $param->{node} && $param->{node} ne $nodename;
raise_param_exc({ stdout => "option is only allowed on the command line interface."})
if $param->{stdout};
}
raise_param_exc({
stdout => "option is only allowed on the command line interface." })
if $param->{stdout};
}
assert_param_permission_vzdump($rpcenv, $user, $param);
assert_param_permission_vzdump($rpcenv, $user, $param);
PVE::VZDump::verify_vzdump_parameters($param, 1);
PVE::VZDump::verify_vzdump_parameters($param, 1);
# silent exit if we run on wrong node
return 'OK' if $param->{node} && $param->{node} ne $nodename;
# silent exit if we run on wrong node
return 'OK' if $param->{node} && $param->{node} ne $nodename;
my $cmdline = PVE::VZDump::Common::command_line($param);
my $cmdline = PVE::VZDump::Common::command_line($param);
my $vmids_per_node = PVE::VZDump::get_included_guests($param);
my $vmids_per_node = PVE::VZDump::get_included_guests($param);
my $local_vmids = delete $vmids_per_node->{$nodename} // [];
my $local_vmids = delete $vmids_per_node->{$nodename} // [];
# include IDs for deleted guests, and visibly fail later
my $orphaned_vmids = delete $vmids_per_node->{''} // [];
push @{$local_vmids}, @{$orphaned_vmids};
# include IDs for deleted guests, and visibly fail later
my $orphaned_vmids = delete $vmids_per_node->{''} // [];
push @{$local_vmids}, @{$orphaned_vmids};
my $skiplist = [ map { @$_ } values $vmids_per_node->%* ];
my $skiplist = [map { @$_ } values $vmids_per_node->%*];
if($param->{stop}){
PVE::VZDump::stop_running_backups();
return 'OK' if !scalar(@{$local_vmids});
}
if ($param->{stop}) {
PVE::VZDump::stop_running_backups();
return 'OK' if !scalar(@{$local_vmids});
}
# silent exit if specified VMs run on other nodes
return "OK" if !scalar(@{$local_vmids}) && !$param->{all};
# silent exit if specified VMs run on other nodes
return "OK" if !scalar(@{$local_vmids}) && !$param->{all};
PVE::VZDump::parse_mailto_exclude_path($param);
PVE::VZDump::parse_mailto_exclude_path($param);
die "you can only backup a single VM with option --stdout\n"
if $param->{stdout} && scalar(@{$local_vmids}) != 1;
die "you can only backup a single VM with option --stdout\n"
if $param->{stdout} && scalar(@{$local_vmids}) != 1;
if (my $storeid = PVE::VZDump::get_storage_param($param)) {
$rpcenv->check($user, "/storage/$storeid", [ 'Datastore.AllocateSpace' ]);
}
if (my $storeid = PVE::VZDump::get_storage_param($param)) {
$rpcenv->check($user, "/storage/$storeid", ['Datastore.AllocateSpace']);
}
my $worker = sub {
my $upid = shift;
my $worker = sub {
my $upid = shift;
$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
die "interrupted by signal\n";
};
$SIG{INT} = $SIG{TERM} = $SIG{QUIT} = $SIG{HUP} = $SIG{PIPE} = sub {
die "interrupted by signal\n";
};
$param->{vmids} = $local_vmids;
my $vzdump = PVE::VZDump->new($cmdline, $param, $skiplist);
$param->{vmids} = $local_vmids;
my $vzdump = PVE::VZDump->new($cmdline, $param, $skiplist);
my $LOCK_FH = eval {
$vzdump->getlock($upid); # only one process allowed
};
if (my $err = $@) {
$vzdump->send_notification([], 0, $err);
exit(-1);
}
my $LOCK_FH = eval {
$vzdump->getlock($upid); # only one process allowed
};
if (my $err = $@) {
$vzdump->send_notification([], 0, $err);
exit(-1);
}
if (defined($param->{ionice})) {
if ($param->{ionice} > 7) {
PVE::VZDump::run_command(undef, "ionice -c3 -p $$");
} else {
PVE::VZDump::run_command(undef, "ionice -c2 -n$param->{ionice} -p $$");
}
}
$vzdump->exec_backup($rpcenv, $user);
if (defined($param->{ionice})) {
if ($param->{ionice} > 7) {
PVE::VZDump::run_command(undef, "ionice -c3 -p $$");
} else {
PVE::VZDump::run_command(undef, "ionice -c2 -n$param->{ionice} -p $$");
}
}
$vzdump->exec_backup($rpcenv, $user);
close($LOCK_FH);
};
close($LOCK_FH);
};
open STDOUT, '>/dev/null' if $param->{quiet} && !$param->{stdout};
open STDERR, '>/dev/null' if $param->{quiet};
open STDOUT, '>/dev/null' if $param->{quiet} && !$param->{stdout};
open STDERR, '>/dev/null' if $param->{quiet};
if ($rpcenv->{type} eq 'cli') {
if ($param->{stdout}) {
if ($rpcenv->{type} eq 'cli') {
if ($param->{stdout}) {
open my $saved_stdout, ">&STDOUT"
|| die "can't dup STDOUT: $!\n";
open my $saved_stdout, ">&STDOUT"
|| die "can't dup STDOUT: $!\n";
open STDOUT, '>&STDERR' ||
die "unable to redirect STDOUT: $!\n";
open STDOUT, '>&STDERR'
|| die "unable to redirect STDOUT: $!\n";
$param->{stdout} = $saved_stdout;
}
}
$param->{stdout} = $saved_stdout;
}
}
my $taskid;
$taskid = $local_vmids->[0] if scalar(@{$local_vmids}) == 1;
my $taskid;
$taskid = $local_vmids->[0] if scalar(@{$local_vmids}) == 1;
return $rpcenv->fork_worker('vzdump', $taskid, $user, $worker);
}});
return $rpcenv->fork_worker('vzdump', $taskid, $user, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'defaults',
path => 'defaults',
method => 'GET',
description => "Get the currently configured vzdump defaults.",
permissions => {
description => "The user needs 'Datastore.Audit' or 'Datastore.AllocateSpace' " .
"permissions for the specified storage (or default storage if none specified). Some " .
"properties are only returned when the user has 'Sys.Audit' permissions for the node.",
user => 'all',
description => "The user needs 'Datastore.Audit' or 'Datastore.AllocateSpace' "
. "permissions for the specified storage (or default storage if none specified). Some "
. "properties are only returned when the user has 'Sys.Audit' permissions for the node.",
user => 'all',
},
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id', { optional => 1 }),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id', { optional => 1 }),
},
},
returns => {
type => 'object',
additionalProperties => 0,
properties => PVE::VZDump::Common::json_config_properties(),
type => 'object',
additionalProperties => 0,
properties => PVE::VZDump::Common::json_config_properties(),
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $node = extract_param($param, 'node');
my $storage = extract_param($param, 'storage');
my $node = extract_param($param, 'node');
my $storage = extract_param($param, 'storage');
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $res = PVE::VZDump::read_vzdump_defaults();
my $res = PVE::VZDump::read_vzdump_defaults();
$res->{storage} = $storage if defined($storage);
$res->{storage} = $storage if defined($storage);
if (!defined($res->{dumpdir}) && !defined($res->{storage})) {
$res->{storage} = 'local';
}
if (!defined($res->{dumpdir}) && !defined($res->{storage})) {
$res->{storage} = 'local';
}
if (defined($res->{storage})) {
$rpcenv->check_any(
$authuser,
"/storage/$res->{storage}",
['Datastore.Audit', 'Datastore.AllocateSpace'],
);
if (defined($res->{storage})) {
$rpcenv->check_any(
$authuser,
"/storage/$res->{storage}",
['Datastore.Audit', 'Datastore.AllocateSpace'],
);
my $info = PVE::VZDump::storage_info($res->{storage});
for my $key (qw(dumpdir prune-backups)) {
$res->{$key} = $info->{$key} if defined($info->{$key});
}
}
my $info = PVE::VZDump::storage_info($res->{storage});
for my $key (qw(dumpdir prune-backups)) {
$res->{$key} = $info->{$key} if defined($info->{$key});
}
}
if (defined($res->{'prune-backups'})) {
$res->{'prune-backups'} = PVE::JSONSchema::print_property_string(
$res->{'prune-backups'},
'prune-backups',
);
}
if (defined($res->{'prune-backups'})) {
$res->{'prune-backups'} = PVE::JSONSchema::print_property_string(
$res->{'prune-backups'}, 'prune-backups',
);
}
$res->{mailto} = join(",", @{$res->{mailto}})
if defined($res->{mailto});
$res->{mailto} = join(",", @{ $res->{mailto} })
if defined($res->{mailto});
$res->{'exclude-path'} = join(",", @{$res->{'exclude-path'}})
if defined($res->{'exclude-path'});
$res->{'exclude-path'} = join(",", @{ $res->{'exclude-path'} })
if defined($res->{'exclude-path'});
# normal backup users don't need to know these
if (!$rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'], 1)) {
delete $res->{mailto};
delete $res->{tmpdir};
delete $res->{dumpdir};
delete $res->{script};
delete $res->{ionice};
}
# normal backup users don't need to know these
if (!$rpcenv->check($authuser, "/nodes/$node", ['Sys.Audit'], 1)) {
delete $res->{mailto};
delete $res->{tmpdir};
delete $res->{dumpdir};
delete $res->{script};
delete $res->{ionice};
}
my $pool = $res->{pool};
if (defined($pool) &&
!$rpcenv->check($authuser, "/pool/$pool", ['Pool.Audit'], 1)) {
delete $res->{pool};
}
my $pool = $res->{pool};
if (
defined($pool)
&& !$rpcenv->check($authuser, "/pool/$pool", ['Pool.Audit'], 1)
) {
delete $res->{pool};
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'extractconfig',
path => 'extractconfig',
method => 'GET',
description => "Extract configuration from vzdump backup archive.",
permissions => {
description => "The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
user => 'all',
description =>
"The user needs 'VM.Backup' permissions on the backed up guest ID, and 'Datastore.AllocateSpace' on the backup storage.",
user => 'all',
},
protected => 1,
proxyto => 'node',
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
volume => {
description => "Volume identifier",
type => 'string',
completion => \&PVE::Storage::complete_volume,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
volume => {
description => "Volume identifier",
type => 'string',
completion => \&PVE::Storage::complete_volume,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $volume = extract_param($param, 'volume');
my $volume = extract_param($param, 'volume');
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $storage_cfg = PVE::Storage::config();
PVE::Storage::check_volume_access(
$rpcenv,
$authuser,
$storage_cfg,
undef,
$volume,
'backup',
);
my $storage_cfg = PVE::Storage::config();
PVE::Storage::check_volume_access(
$rpcenv, $authuser, $storage_cfg, undef, $volume, 'backup',
);
if (PVE::Storage::parse_volume_id($volume, 1)) {
my (undef, undef, $ownervm) = PVE::Storage::parse_volname($storage_cfg, $volume);
$rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
}
if (PVE::Storage::parse_volume_id($volume, 1)) {
my (undef, undef, $ownervm) = PVE::Storage::parse_volname($storage_cfg, $volume);
$rpcenv->check($authuser, "/vms/$ownervm", ['VM.Backup']);
}
return PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
}});
return PVE::Storage::extract_vzdump_config($storage_cfg, $volume);
},
});
1;

View file

@ -25,16 +25,18 @@ sub get_hwaddress {
my $fn = '/etc/ssh/ssh_host_rsa_key.pub';
my $st = stat($fn);
if (defined($hwaddress)
&& $hwaddress_st->{mtime} == $st->mtime
&& $hwaddress_st->{ino} == $st->ino
&& $hwaddress_st->{dev} == $st->dev) {
return $hwaddress;
if (
defined($hwaddress)
&& $hwaddress_st->{mtime} == $st->mtime
&& $hwaddress_st->{ino} == $st->ino
&& $hwaddress_st->{dev} == $st->dev
) {
return $hwaddress;
}
my $sshkey = PVE::Tools::file_get_contents($fn);
$hwaddress = uc(md5_hex($sshkey));
$hwaddress_st->@{'mtime', 'ino', 'dev'} = ($st->mtime, $st->ino, $st->dev);
$hwaddress_st->@{ 'mtime', 'ino', 'dev' } = ($st->mtime, $st->ino, $st->dev);
return $hwaddress;
}
@ -43,35 +45,40 @@ sub extract_node_stats {
my ($node, $members, $rrd, $exclude_stats) = @_;
my $entry = {
id => "node/$node",
node => $node,
type => "node",
status => 'unknown',
id => "node/$node",
node => $node,
type => "node",
status => 'unknown',
};
if (my $d = $rrd->{"pve2-node/$node"}) {
if (!$members || # no cluster
($members->{$node} && $members->{$node}->{online})) {
if (!$exclude_stats) {
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{cpu} = ($d->[5] || 0) + 0;
$entry->{mem} = ($d->[8] || 0) + 0;
$entry->{disk} = ($d->[12] || 0) + 0;
}
$entry->{status} = 'online';
}
$entry->{level} = $d->[1];
if (!$exclude_stats) {
$entry->{maxcpu} = ($d->[4] || 0) + 0;
$entry->{maxmem} = ($d->[7] || 0) + 0;
$entry->{maxdisk} = ($d->[11] || 0) + 0;
}
if (
!$members || # no cluster
($members->{$node} && $members->{$node}->{online})
) {
if (!$exclude_stats) {
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{cpu} = ($d->[5] || 0) + 0;
$entry->{mem} = ($d->[8] || 0) + 0;
$entry->{disk} = ($d->[12] || 0) + 0;
}
$entry->{status} = 'online';
}
$entry->{level} = $d->[1];
if (!$exclude_stats) {
$entry->{maxcpu} = ($d->[4] || 0) + 0;
$entry->{maxmem} = ($d->[7] || 0) + 0;
$entry->{maxdisk} = ($d->[11] || 0) + 0;
}
}
if ($members && $members->{$node} &&
!$members->{$node}->{online}) {
$entry->{status} = 'offline';
if (
$members
&& $members->{$node}
&& !$members->{$node}->{online}
) {
$entry->{status} = 'offline';
}
return $entry;
@ -81,49 +88,49 @@ sub extract_vm_stats {
my ($vmid, $data, $rrd) = @_;
my $entry = {
id => "$data->{type}/$vmid",
vmid => $vmid + 0,
node => $data->{node},
type => $data->{type},
status => 'unknown',
id => "$data->{type}/$vmid",
vmid => $vmid + 0,
node => $data->{node},
type => $data->{type},
status => 'unknown',
};
my $d;
if ($d = $rrd->{"pve2-vm/$vmid"}) {
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{name} = $d->[1];
$entry->{status} = $entry->{uptime} ? 'running' : 'stopped';
$entry->{maxcpu} = ($d->[3] || 0) + 0;
$entry->{cpu} = ($d->[4] || 0) + 0;
$entry->{maxmem} = ($d->[5] || 0) + 0;
$entry->{mem} = ($d->[6] || 0) + 0;
$entry->{maxdisk} = ($d->[7] || 0) + 0;
$entry->{disk} = ($d->[8] || 0) + 0;
$entry->{netin} = ($d->[9] || 0) + 0;
$entry->{netout} = ($d->[10] || 0) + 0;
$entry->{diskread} = ($d->[11] || 0) + 0;
$entry->{diskwrite} = ($d->[12] || 0) + 0;
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{name} = $d->[1];
$entry->{status} = $entry->{uptime} ? 'running' : 'stopped';
$entry->{maxcpu} = ($d->[3] || 0) + 0;
$entry->{cpu} = ($d->[4] || 0) + 0;
$entry->{maxmem} = ($d->[5] || 0) + 0;
$entry->{mem} = ($d->[6] || 0) + 0;
$entry->{maxdisk} = ($d->[7] || 0) + 0;
$entry->{disk} = ($d->[8] || 0) + 0;
$entry->{netin} = ($d->[9] || 0) + 0;
$entry->{netout} = ($d->[10] || 0) + 0;
$entry->{diskread} = ($d->[11] || 0) + 0;
$entry->{diskwrite} = ($d->[12] || 0) + 0;
} elsif ($d = $rrd->{"pve2.3-vm/$vmid"}) {
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{name} = $d->[1];
$entry->{status} = $d->[2];
$entry->{template} = $d->[3] + 0;
$entry->{uptime} = ($d->[0] || 0) + 0;
$entry->{name} = $d->[1];
$entry->{status} = $d->[2];
$entry->{template} = $d->[3] + 0;
$entry->{maxcpu} = ($d->[5] || 0) + 0;
$entry->{cpu} = ($d->[6] || 0) + 0;
$entry->{maxmem} = ($d->[7] || 0) + 0;
$entry->{mem} = ($d->[8] || 0) + 0;
$entry->{maxdisk} = ($d->[9] || 0) + 0;
$entry->{disk} = ($d->[10] || 0) + 0;
$entry->{netin} = ($d->[11] || 0) + 0;
$entry->{netout} = ($d->[12] || 0) + 0;
$entry->{diskread} = ($d->[13] || 0) + 0;
$entry->{diskwrite} = ($d->[14] || 0) + 0;
};
$entry->{maxcpu} = ($d->[5] || 0) + 0;
$entry->{cpu} = ($d->[6] || 0) + 0;
$entry->{maxmem} = ($d->[7] || 0) + 0;
$entry->{mem} = ($d->[8] || 0) + 0;
$entry->{maxdisk} = ($d->[9] || 0) + 0;
$entry->{disk} = ($d->[10] || 0) + 0;
$entry->{netin} = ($d->[11] || 0) + 0;
$entry->{netout} = ($d->[12] || 0) + 0;
$entry->{diskread} = ($d->[13] || 0) + 0;
$entry->{diskwrite} = ($d->[14] || 0) + 0;
}
return $entry;
}
@ -134,20 +141,20 @@ sub extract_storage_stats {
my $content = PVE::Storage::Plugin::content_hash_to_string($scfg->{content});
my $entry = {
id => "storage/$node/$storeid",
storage => $storeid,
node => $node,
type => 'storage',
plugintype => $scfg->{type},
status => 'unknown',
shared => $scfg->{shared} || 0,
content => $content,
id => "storage/$node/$storeid",
storage => $storeid,
node => $node,
type => 'storage',
plugintype => $scfg->{type},
status => 'unknown',
shared => $scfg->{shared} || 0,
content => $content,
};
if (my $d = $rrd->{"pve2-storage/$node/$storeid"}) {
$entry->{maxdisk} = ($d->[1] || 0) + 0;
$entry->{disk} = ($d->[2] || 0) + 0;
$entry->{status} = 'available';
$entry->{maxdisk} = ($d->[1] || 0) + 0;
$entry->{disk} = ($d->[2] || 0) + 0;
$entry->{status} = 'available';
}
return $entry;
@ -165,7 +172,7 @@ sub parse_http_proxy {
my ($username, $password);
if (defined(my $p_auth = $uri->userinfo())) {
($username, $password) = map URI::Escape::uri_unescape($_), split(":", $p_auth, 2);
($username, $password) = map URI::Escape::uri_unescape($_), split(":", $p_auth, 2);
}
return ("$host:$port", $username, $password);
@ -183,13 +190,23 @@ sub run_spiceterm {
my $port = PVE::Tools::next_spice_port($family);
my ($ticket, undef, $remote_viewer_config) =
PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
PVE::AccessControl::remote_viewer_config($authuser, $vmid, $node, $proxy, $title, $port);
my $timeout = 40;
my $cmd = ['/usr/bin/spiceterm', '--port', $port, '--addr', 'localhost',
'--timeout', $timeout, '--authpath', $authpath,
'--permissions', $permissions];
my $cmd = [
'/usr/bin/spiceterm',
'--port',
$port,
'--addr',
'localhost',
'--timeout',
$timeout,
'--authpath',
$authpath,
'--permissions',
$permissions,
];
my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
push @$cmd, '--keymap', $dcconf->{keyboard} if $dcconf->{keyboard};
@ -197,34 +214,40 @@ sub run_spiceterm {
push @$cmd, '--', @$shcmd;
my $realcmd = sub {
my $upid = shift;
my $upid = shift;
syslog ('info', "starting spiceterm $upid - $title\n");
syslog('info', "starting spiceterm $upid - $title\n");
my $cmdstr = join (' ', @$cmd);
syslog ('info', "launch command: $cmdstr");
my $cmdstr = join(' ', @$cmd);
syslog('info', "launch command: $cmdstr");
eval {
foreach my $k (keys %ENV) {
next if $k eq 'PATH' || $k eq 'TERM' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE' ;
delete $ENV{$k};
}
$ENV{PWD} = '/';
$ENV{SPICE_TICKET} = $ticket;
eval {
foreach my $k (keys %ENV) {
next
if $k eq 'PATH'
|| $k eq 'TERM'
|| $k eq 'USER'
|| $k eq 'HOME'
|| $k eq 'LANG'
|| $k eq 'LANGUAGE';
delete $ENV{$k};
}
$ENV{PWD} = '/';
$ENV{SPICE_TICKET} = $ticket;
PVE::Tools::run_command($cmd, errmsg => 'spiceterm failed\n', keeplocale => 1);
};
if (my $err = $@) {
syslog ('err', $err);
}
PVE::Tools::run_command($cmd, errmsg => 'spiceterm failed\n', keeplocale => 1);
};
if (my $err = $@) {
syslog('err', $err);
}
return;
return;
};
if ($vmid) {
$rpcenv->fork_worker('spiceproxy', $vmid, $authuser, $realcmd);
$rpcenv->fork_worker('spiceproxy', $vmid, $authuser, $realcmd);
} else {
$rpcenv->fork_worker('spiceshell', undef, $authuser, $realcmd);
$rpcenv->fork_worker('spiceshell', undef, $authuser, $realcmd);
}
PVE::Tools::wait_for_vnc_port($port);
@ -237,13 +260,13 @@ sub resolve_proxyto {
my $node;
if ($proxyto_callback) {
$node = $proxyto_callback->($rpcenv, $proxyto, $uri_param);
die "internal error - proxyto_callback returned nothing\n"
if !$node;
$node = $proxyto_callback->($rpcenv, $proxyto, $uri_param);
die "internal error - proxyto_callback returned nothing\n"
if !$node;
} else {
$node = $uri_param->{$proxyto};
raise_param_exc({ $proxyto => "proxyto parameter does not exist"})
if !$node;
$node = $uri_param->{$proxyto};
raise_param_exc({ $proxyto => "proxyto parameter does not exist" })
if !$node;
}
return $node;
}
@ -260,7 +283,7 @@ sub get_resource_pool_guest_members {
die "pool '$pool' does not exist\n" if !$data;
my $pool_members = [ grep { $idlist->{$_} } keys %{$data->{vms}} ];
my $pool_members = [grep { $idlist->{$_} } keys %{ $data->{vms} }];
return $pool_members;
}

View file

@ -20,10 +20,10 @@ sub logmsg {
chomp $msg;
my $tstr = strftime ("%F %H:%M:%S", localtime);
my $tstr = strftime("%F %H:%M:%S", localtime);
foreach my $line (split (/\n/, $msg)) {
print $logfd "$tstr $line\n";
foreach my $line (split(/\n/, $msg)) {
print $logfd "$tstr $line\n";
}
}
@ -33,84 +33,89 @@ sub read_aplinfo_from_fh {
local $/ = "";
while (my $rec = <$fh>) {
chomp $rec;
chomp $rec;
my $res = {};
my $res = {};
while ($rec) {
while ($rec) {
if ($rec =~ s/^Description:\s*([^\n]*)(\n\s+.*)*$//si) {
$res->{headline} = $1;
my $long = $2 || '';
$long =~ s/\n\s+/ /g;
$long =~ s/^\s+//g;
$long =~ s/\s+$//g;
$res->{description} = $long;
} elsif ($rec =~ s/^Version:\s*(.*\S)\s*\n//i) {
my $version = $1;
if ($version =~ m/^(\d[a-zA-Z0-9\.\+\-\:\~]*)(-(\d+))?$/) {
$res->{version} = $version;
} else {
my $msg = "unable to parse appliance record: version = '$version'\n";
$update ? die $msg : warn $msg;
}
} elsif ($rec =~ s/^Type:\s*(.*\S)\s*\n//i) {
my $type = $1;
if ($type =~ m/^(openvz|lxc)$/) {
$res->{type} = $type;
} else {
my $msg = "unable to parse appliance record: unknown type '$type'\n";
$update ? die $msg : warn $msg;
}
} elsif ($rec =~ s/^([^:]+):\s*(.*\S)\s*\n//) {
$res->{lc $1} = $2;
} else {
my $msg = "unable to parse appliance record: $rec\n";
$update ? die $msg : warn $msg;
$res = {};
last;
}
}
if ($rec =~ s/^Description:\s*([^\n]*)(\n\s+.*)*$//si) {
$res->{headline} = $1;
my $long = $2 || '';
$long =~ s/\n\s+/ /g;
$long =~ s/^\s+//g;
$long =~ s/\s+$//g;
$res->{description} = $long;
} elsif ($rec =~ s/^Version:\s*(.*\S)\s*\n//i) {
my $version = $1;
if ($version =~ m/^(\d[a-zA-Z0-9\.\+\-\:\~]*)(-(\d+))?$/) {
$res->{version} = $version;
} else {
my $msg = "unable to parse appliance record: version = '$version'\n";
$update ? die $msg : warn $msg;
}
} elsif ($rec =~ s/^Type:\s*(.*\S)\s*\n//i) {
my $type = $1;
if ($type =~ m/^(openvz|lxc)$/) {
$res->{type} = $type;
} else {
my $msg = "unable to parse appliance record: unknown type '$type'\n";
$update ? die $msg : warn $msg;
}
} elsif ($rec =~ s/^([^:]+):\s*(.*\S)\s*\n//) {
$res->{ lc $1 } = $2;
} else {
my $msg = "unable to parse appliance record: $rec\n";
$update ? die $msg : warn $msg;
$res = {};
last;
}
}
if ($res->{'package'} eq 'pve-web-news' && $res->{description}) {
$list->{'all'}->{$res->{'package'}} = $res;
next;
}
if ($res->{'package'} eq 'pve-web-news' && $res->{description}) {
$list->{'all'}->{ $res->{'package'} } = $res;
next;
}
$res->{section} = 'unknown' if !$res->{section};
$res->{section} = 'unknown' if !$res->{section};
if ($res->{'package'} && $res->{type} && $res->{os} && $res->{version} &&
$res->{infopage}) {
my $template;
if ($res->{location}) {
$template = $res->{location};
$template =~ s|.*/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$|$1|;
if ($res->{location} !~ m|^([a-zA-Z]+)\://|) {
# relative location (no http:// prefix)
$res->{location} = "$source/$res->{location}";
}
} else {
my $arch = $res->{architecture} || 'i386';
$template = "$res->{os}-$res->{package}_$res->{version}_$arch.tar.gz";
$template =~ s/$res->{os}-$res->{os}-/$res->{os}-/;
$res->{location} = "$source/$res->{section}/$template";
}
$res->{source} = $source;
$res->{template} = $template;
$list->{$res->{section}}->{$template} = $res;
$list->{'all'}->{$template} = $res;
} else {
my $msg = "found incomplete appliance records\n";
$update ? die $msg : warn $msg;
}
if (
$res->{'package'}
&& $res->{type}
&& $res->{os}
&& $res->{version}
&& $res->{infopage}
) {
my $template;
if ($res->{location}) {
$template = $res->{location};
$template =~ s|.*/([^/]+$PVE::Storage::VZTMPL_EXT_RE_1)$|$1|;
if ($res->{location} !~ m|^([a-zA-Z]+)\://|) {
# relative location (no http:// prefix)
$res->{location} = "$source/$res->{location}";
}
} else {
my $arch = $res->{architecture} || 'i386';
$template = "$res->{os}-$res->{package}_$res->{version}_$arch.tar.gz";
$template =~ s/$res->{os}-$res->{os}-/$res->{os}-/;
$res->{location} = "$source/$res->{section}/$template";
}
$res->{source} = $source;
$res->{template} = $template;
$list->{ $res->{section} }->{$template} = $res;
$list->{'all'}->{$template} = $res;
} else {
my $msg = "found incomplete appliance records\n";
$update ? die $msg : warn $msg;
}
}
}
sub read_aplinfo {
my ($filename, $list, $source, $update) = @_;
my $fh = IO::File->new("<$filename") ||
die "unable to open file '$filename' - $!\n";
my $fh = IO::File->new("<$filename")
|| die "unable to open file '$filename' - $!\n";
eval { read_aplinfo_from_fh($fh, $list, $source, $update); };
my $err = $@;
@ -127,15 +132,15 @@ sub url_get {
my $req = HTTP::Request->new(GET => $url);
logmsg ($logfh, "start download $url");
logmsg($logfh, "start download $url");
my $res = $ua->request($req, $file);
if ($res->is_success) {
logmsg ($logfh, "download finished: " . $res->status_line);
return 0;
logmsg($logfh, "download finished: " . $res->status_line);
return 0;
}
logmsg ($logfh, "download failed: " . $res->status_line);
logmsg($logfh, "download failed: " . $res->status_line);
return 1;
}
@ -153,35 +158,33 @@ sub download_aplinfo {
eval {
if (url_get($ua, $aplsigurl, $sigfn, $logfd) != 0) {
die "update failed - no signature file '$sigfn'\n";
}
if (url_get($ua, $aplsigurl, $sigfn, $logfd) != 0) {
die "update failed - no signature file '$sigfn'\n";
}
if (url_get($ua, $aplsrcurl, $tmpgz, $logfd) != 0) {
die "update failed - no data file '$aplsrcurl'\n";
}
if (url_get($ua, $aplsrcurl, $tmpgz, $logfd) != 0) {
die "update failed - no data file '$aplsrcurl'\n";
}
eval { run_command(["gunzip", "-f", $tmpgz]) };
die "update failed: unable to unpack '$tmpgz'\n" if $@;
eval { run_command(["gunzip", "-f", $tmpgz]) };
die "update failed: unable to unpack '$tmpgz'\n" if $@;
# verify signature
my $trustedkeyring = "/usr/share/doc/pve-manager/trustedkeys.gpg";
my $cmd = "/usr/bin/gpgv -q --keyring $trustedkeyring $sigfn $tmp";
# verify signature
my $trustedkeyring = "/usr/share/doc/pve-manager/trustedkeys.gpg";
my $cmd = "/usr/bin/gpgv -q --keyring $trustedkeyring $sigfn $tmp";
my $logfunc = sub { logmsg($logfd, "signature verification: $_[0]"); };
eval {
run_command($cmd, outfunc => $logfunc, errfunc => $logfunc);
};
die "unable to verify signature - $@\n" if $@;
my $logfunc = sub { logmsg($logfd, "signature verification: $_[0]"); };
eval { run_command($cmd, outfunc => $logfunc, errfunc => $logfunc); };
die "unable to verify signature - $@\n" if $@;
# test syntax
eval { read_aplinfo($tmp, {}, $aplinfo->{url}, 1) };
die "update failed: $@" if $@;
# test syntax
eval { read_aplinfo($tmp, {}, $aplinfo->{url}, 1) };
die "update failed: $@" if $@;
rename($tmp, "$APL_INFO_DIRECTORY/$host") or
die "update failed: unable to store data: $!\n";
rename($tmp, "$APL_INFO_DIRECTORY/$host")
or die "update failed: unable to store data: $!\n";
logmsg($logfd, "update successful");
logmsg($logfd, "update successful");
};
my $err = $@;
@ -195,16 +198,16 @@ sub download_aplinfo {
sub get_apl_sources {
my $sources = [
{
host => "download.proxmox.com",
url => "http://download.proxmox.com/images",
file => 'aplinfo-pve-8.dat',
},
{
host => "releases.turnkeylinux.org",
url => "https://releases.turnkeylinux.org/pve",
file => 'aplinfo.dat',
},
{
host => "download.proxmox.com",
url => "http://download.proxmox.com/images",
file => 'aplinfo-pve-8.dat',
},
{
host => "releases.turnkeylinux.org",
url => "https://releases.turnkeylinux.org/pve",
file => 'aplinfo.dat',
},
];
return $sources;
@ -215,9 +218,9 @@ sub update {
my $logfile_size = -s $LOGFILE || 0;
if ($logfile_size > 1024 * 256) {
rename($LOGFILE, "$LOGFILE.0") or warn "failed to rotate log file $LOGFILE - $!\n";
rename($LOGFILE, "$LOGFILE.0") or warn "failed to rotate log file $LOGFILE - $!\n";
}
my $logfd = IO::File->new (">>$LOGFILE");
my $logfd = IO::File->new(">>$LOGFILE");
logmsg($logfd, "starting update");
my $ua = LWP::UserAgent->new;
@ -225,9 +228,9 @@ sub update {
$ua->agent("PVE/$release");
if ($proxy) {
$ua->proxy(['http', 'https'], $proxy);
$ua->proxy(['http', 'https'], $proxy);
} else {
$ua->env_proxy;
$ua->env_proxy;
}
my $sources = get_apl_sources();
@ -236,13 +239,11 @@ sub update {
my @dlerr = ();
foreach my $info (@$sources) {
eval {
download_aplinfo($ua, $info, $logfd);
};
if (my $err = $@) {
logmsg ($logfd, $err);
push @dlerr, $info->{url};
}
eval { download_aplinfo($ua, $info, $logfd); };
if (my $err = $@) {
logmsg($logfd, $err);
push @dlerr, $info->{url};
}
}
close($logfd);
@ -254,15 +255,15 @@ sub update {
sub load_data {
my $sources = get_apl_sources();
my $sources = get_apl_sources();
my $list = {};
foreach my $info (@$sources) {
eval {
my $host = $info->{host};
read_aplinfo("$APL_INFO_DIRECTORY/$host", $list, $info->{url});
};
warn $@ if $@;
eval {
my $host = $info->{host};
read_aplinfo("$APL_INFO_DIRECTORY/$host", $list, $info->{url});
};
warn $@ if $@;
}
return $list;

View file

@ -4,144 +4,148 @@ use warnings;
use strict;
sub compute_alg1 {
my ($vmstatus, $goal, $maxchange, $debug) = @_;
my ($vmstatus, $goal, $maxchange, $debug) = @_;
my $log = sub { print @_ if $debug; };
my $change_func = sub {
my ($res, $idlist, $bytes) = @_;
my ($res, $idlist, $bytes) = @_;
my $rest = $bytes;
my $repeat = 1;
my $done_hash = {};
my $progress = 1;
my $rest = $bytes;
my $repeat = 1;
my $done_hash = {};
my $progress = 1;
while ($rest && $repeat && $progress) {
$repeat = 0;
$progress = 0;
while ($rest && $repeat && $progress) {
$repeat = 0;
$progress = 0;
my $shares_total = 0;
my $alloc_old = 0;
my $shares_total = 0;
my $alloc_old = 0;
foreach my $vmid (@$idlist) {
next if defined($done_hash->{$vmid});
my $d = $vmstatus->{$vmid};
my $balloon = defined($res->{$vmid}) ? $res->{$vmid} : $d->{balloon};
$alloc_old += $balloon - $d->{balloon_min};
$shares_total += $d->{shares} || 1000;
}
foreach my $vmid (@$idlist) {
next if defined($done_hash->{$vmid});
my $d = $vmstatus->{$vmid};
my $balloon = defined($res->{$vmid}) ? $res->{$vmid} : $d->{balloon};
$alloc_old += $balloon - $d->{balloon_min};
$shares_total += $d->{shares} || 1000;
}
my $changes = 0;
my $changes = 0;
my $alloc_new = $alloc_old + $rest;
my $alloc_new = $alloc_old + $rest;
&$log("shares_total: $shares_total $alloc_new\n");
&$log("shares_total: $shares_total $alloc_new\n");
foreach my $vmid (@$idlist) {
next if defined($done_hash->{$vmid});
my $d = $vmstatus->{$vmid};
my $shares = $d->{shares} || 1000;
my $desired = $d->{balloon_min} + int(($alloc_new/$shares_total)*$shares);
foreach my $vmid (@$idlist) {
next if defined($done_hash->{$vmid});
my $d = $vmstatus->{$vmid};
my $shares = $d->{shares} || 1000;
my $desired = $d->{balloon_min} + int(($alloc_new / $shares_total) * $shares);
if ($desired > $d->{maxmem}) {
$desired = $d->{maxmem};
$repeat = 1;
} elsif ($desired < $d->{balloon_min}) {
$desired = $d->{balloon_min};
$repeat = 1;
}
if ($desired > $d->{maxmem}) {
$desired = $d->{maxmem};
$repeat = 1;
} elsif ($desired < $d->{balloon_min}) {
$desired = $d->{balloon_min};
$repeat = 1;
}
my ($new, $balloon);
if (($bytes > 0) && ($desired - $d->{balloon}) > 0) { # grow
$new = $d->{balloon} + $maxchange;
$balloon = $new > $desired ? $desired : $new;
} elsif (($desired - $d->{balloon}) < 0) { # shrink
$new = $d->{balloon} - $maxchange;
$balloon = $new > $desired ? $new : $desired;
} else {
$done_hash->{$vmid} = 1;
next;
}
my ($new, $balloon);
if (($bytes > 0) && ($desired - $d->{balloon}) > 0) { # grow
$new = $d->{balloon} + $maxchange;
$balloon = $new > $desired ? $desired : $new;
} elsif (($desired - $d->{balloon}) < 0) { # shrink
$new = $d->{balloon} - $maxchange;
$balloon = $new > $desired ? $new : $desired;
} else {
$done_hash->{$vmid} = 1;
next;
}
my $diff = $balloon - $d->{balloon};
if ($diff != 0) {
my $oldballoon = defined($res->{$vmid}) ? $res->{$vmid} : $d->{balloon};
$res->{$vmid} = $balloon;
my $change = $balloon - $oldballoon;
if ($change != 0) {
$changes += $change;
my $absdiff = $diff > 0 ? $diff : -$diff;
$progress += $absdiff;
$repeat = 1;
}
&$log("change request for $vmid ($balloon, $diff, $desired, $new, $changes, $progress)\n");
}
}
my $diff = $balloon - $d->{balloon};
if ($diff != 0) {
my $oldballoon = defined($res->{$vmid}) ? $res->{$vmid} : $d->{balloon};
$res->{$vmid} = $balloon;
my $change = $balloon - $oldballoon;
if ($change != 0) {
$changes += $change;
my $absdiff = $diff > 0 ? $diff : -$diff;
$progress += $absdiff;
$repeat = 1;
}
&$log(
"change request for $vmid ($balloon, $diff, $desired, $new, $changes, $progress)\n"
);
}
}
$rest -= $changes;
}
$rest -= $changes;
}
return $rest;
return $rest;
};
my $idlist = []; # list of VMs with working balloon river
my $idlist1 = []; # list of VMs with memory pressure
my $idlist2 = []; # list of VMs with enough free memory
foreach my $vmid (keys %$vmstatus) {
my $d = $vmstatus->{$vmid};
next if !$d->{balloon}; # skip if balloon driver not running
next if !$d->{balloon_min}; # skip if balloon value not set in config
next if $d->{lock} && $d->{lock} eq 'migrate';
next if defined($d->{shares}) &&
($d->{shares} == 0); # skip if shares set to zero
my $d = $vmstatus->{$vmid};
next if !$d->{balloon}; # skip if balloon driver not running
next if !$d->{balloon_min}; # skip if balloon value not set in config
next if $d->{lock} && $d->{lock} eq 'migrate';
next
if defined($d->{shares})
&& ($d->{shares} == 0); # skip if shares set to zero
push @$idlist, $vmid;
push @$idlist, $vmid;
if ($d->{freemem} &&
($d->{freemem} > $d->{balloon_min}*0.25) &&
($d->{balloon} >= $d->{balloon_min})) {
push @$idlist2, $vmid;
&$log("idlist2 $vmid $d->{balloon}, $d->{balloon_min}, $d->{freemem}\n");
} else {
push @$idlist1, $vmid;
&$log("idlist1 $vmid $d->{balloon}, $d->{balloon_min}\n");
}
if (
$d->{freemem}
&& ($d->{freemem} > $d->{balloon_min} * 0.25)
&& ($d->{balloon} >= $d->{balloon_min})
) {
push @$idlist2, $vmid;
&$log("idlist2 $vmid $d->{balloon}, $d->{balloon_min}, $d->{freemem}\n");
} else {
push @$idlist1, $vmid;
&$log("idlist1 $vmid $d->{balloon}, $d->{balloon_min}\n");
}
}
my $res = {};
if ($goal > 10*1024*1024) {
&$log("grow request start $goal\n");
# priorize VMs with memory pressure
my $rest = &$change_func($res, $idlist1, $goal);
if ($rest >= $goal) { # no progress ==> consider all VMs
&$log("grow request loop $rest\n");
$rest = &$change_func($res, $idlist, $rest);
}
&$log("grow request end $rest\n");
if ($goal > 10 * 1024 * 1024) {
&$log("grow request start $goal\n");
# priorize VMs with memory pressure
my $rest = &$change_func($res, $idlist1, $goal);
if ($rest >= $goal) { # no progress ==> consider all VMs
&$log("grow request loop $rest\n");
$rest = &$change_func($res, $idlist, $rest);
}
&$log("grow request end $rest\n");
} elsif ($goal < -10*1024*1024) {
&$log("shrink request $goal\n");
# priorize VMs with enough free memory
my $rest = &$change_func($res, $idlist2, $goal);
if ($rest <= $goal) { # no progress ==> consider all VMs
&$log("shrink request loop $rest\n");
$rest = &$change_func($res, $idlist, $rest);
}
&$log("shrink request end $rest\n");
} else {
&$log("do nothing\n");
# do nothing - requested change to small
} elsif ($goal < -10 * 1024 * 1024) {
&$log("shrink request $goal\n");
# priorize VMs with enough free memory
my $rest = &$change_func($res, $idlist2, $goal);
if ($rest <= $goal) { # no progress ==> consider all VMs
&$log("shrink request loop $rest\n");
$rest = &$change_func($res, $idlist, $rest);
}
&$log("shrink request end $rest\n");
} else {
&$log("do nothing\n");
# do nothing - requested change to small
}
foreach my $vmid (@$idlist) {
next if !$res->{$vmid};
my $d = $vmstatus->{$vmid};
my $diff = int($res->{$vmid} - $d->{balloon});
my $absdiff = $diff < 0 ? -$diff : $diff;
&$log("BALLOON $vmid to $res->{$vmid} ($diff)\n");
next if !$res->{$vmid};
my $d = $vmstatus->{$vmid};
my $diff = int($res->{$vmid} - $d->{balloon});
my $absdiff = $diff < 0 ? -$diff : $diff;
&$log("BALLOON $vmid to $res->{$vmid} ($diff)\n");
}
return $res;
}

File diff suppressed because it is too large Load diff

View file

@ -24,181 +24,196 @@ sub setup_environment {
PVE::RPCEnvironment->setup_default_cli_env();
}
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'update',
path => 'update',
method => 'PUT',
description => "Update Container Template Database.",
parameters => {
additionalProperties => 0,
properties => {},
additionalProperties => 0,
properties => {},
},
returns => { type => 'null'},
returns => { type => 'null' },
code => sub {
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
my $res = PVE::APLInfo::update($dccfg->{http_proxy});
my $res = PVE::APLInfo::update($dccfg->{http_proxy});
if ($res) {
print STDOUT "update successful\n"
} else {
print STDERR "update failed - see /var/log/pveam.log for details\n"
}
if ($res) {
print STDOUT "update successful\n";
} else {
print STDERR "update failed - see /var/log/pveam.log for details\n";
}
return undef;
return undef;
}});
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'available',
path => 'available',
method => 'GET',
description => "List available templates.",
parameters => {
additionalProperties => 0,
properties => {
section => {
type => 'string',
description => "Restrict list to specified section.",
enum => ['system', 'mail', 'turnkeylinux'],
optional => 1,
},
}
additionalProperties => 0,
properties => {
section => {
type => 'string',
description => "Restrict list to specified section.",
enum => ['system', 'mail', 'turnkeylinux'],
optional => 1,
},
},
},
returns => { type => 'null'},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $list = PVE::APLInfo::load_data();
my $list = PVE::APLInfo::load_data();
foreach my $section (sort keys %$list) {
next if $section eq 'all';
next if $param->{section} && $section ne $param->{section};
foreach my $template (sort keys %{$list->{$section}}) {
print sprintf("%-15s %s\n", $section, $template);
}
}
return undef;
foreach my $section (sort keys %$list) {
next if $section eq 'all';
next if $param->{section} && $section ne $param->{section};
foreach my $template (sort keys %{ $list->{$section} }) {
print sprintf("%-15s %s\n", $section, $template);
}
}
return undef;
}});
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'index',
path => 'index',
method => 'GET',
description => "Get list of all templates on storage",
permissions => {
description => "Show all users the template which have permission on that storage.",
check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
description => "Show all users the template which have permission on that storage.",
check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option('pve-storage-id', {
description => "Only list templates on specified storage",
completion => \&PVE::Storage::complete_storage_enabled,
}),
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
storage => get_standard_option(
'pve-storage-id',
{
description => "Only list templates on specified storage",
completion => \&PVE::Storage::complete_storage_enabled,
},
),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
type => 'array',
items => {
type => "object",
properties => {},
},
},
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
my $storeid = $param->{storage};
my $storeid = $param->{storage};
my $cfg = PVE::Storage::config();
my $cfg = PVE::Storage::config();
PVE::Storage::storage_check_enabled($cfg, $storeid);
PVE::Storage::storage_check_enabled($cfg, $storeid);
die "Storage does not support templates!\n" if !$cfg->{ids}->{$storeid}->{content}->{vztmpl};
die "Storage does not support templates!\n"
if !$cfg->{ids}->{$storeid}->{content}->{vztmpl};
my $vollist = PVE::Storage::volume_list($cfg, $storeid, undef, 'vztmpl');
my $vollist = PVE::Storage::volume_list($cfg, $storeid, undef, 'vztmpl');
my $res = [];
foreach my $item (@$vollist) {
eval { PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $item->{volid}); };
next if $@;
push @$res, $item;
}
my $res = [];
foreach my $item (@$vollist) {
eval {
PVE::Storage::check_volume_access(
$rpcenv, $authuser, $cfg, undef, $item->{volid},
);
};
next if $@;
push @$res, $item;
}
return $res;
}});
return $res;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'remove',
path => 'remove',
method => 'DELETE',
description => "Remove a template.",
permissions => {
description => "Only user who can create templates can remove them.",
check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
description => "Only user who can create templates can remove them.",
check => ['perm', '/storage/{storage}', ['Datastore.AllocateTemplate']],
},
proxyto => 'node',
protected => 1,
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
template_path => {
type => 'string',
description => "The template to remove.",
maxLength => 255,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
template_path => {
type => 'string',
description => "The template to remove.",
maxLength => 255,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $authuser = $rpcenv->get_user();
my $template = $param->{template_path};
my $template = $param->{template_path};
my $cfg = PVE::Storage::config();
my $cfg = PVE::Storage::config();
PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $template, 'vztmpl');
PVE::Storage::check_volume_access($rpcenv, $authuser, $cfg, undef, $template, 'vztmpl');
my $abs_path = PVE::Storage::abs_filesystem_path($cfg, $template);
my $abs_path = PVE::Storage::abs_filesystem_path($cfg, $template);
unlink $abs_path;
return undef;
}});
unlink $abs_path;
return undef;
},
});
my $print_list = sub {
my ($list) = @_;
printf "%-60s %-6s\n",
qw(NAME SIZE);
printf "%-60s %-6s\n", qw(NAME SIZE);
foreach my $rec (@$list) {
printf "%-60s %-4.2fMB\n", $rec->{volid}, $rec->{size}/(1024*1024);
printf "%-60s %-4.2fMB\n", $rec->{volid}, $rec->{size} / (1024 * 1024);
}
};
our $cmddef = {
update => [ __PACKAGE__, 'update', []],
download => [ 'PVE::API2::Nodes::Nodeinfo', 'apl_download', [ 'storage', 'template'], { node => $nodename } ],
available => [ __PACKAGE__, 'available', []],
list => [ __PACKAGE__, 'index', [ 'storage' ], { node => $nodename }, $print_list ],
remove => [ __PACKAGE__, 'remove', [ 'template_path' ], { node => $nodename }]
update => [__PACKAGE__, 'update', []],
download => [
'PVE::API2::Nodes::Nodeinfo',
'apl_download',
['storage', 'template'],
{ node => $nodename },
],
available => [__PACKAGE__, 'available', []],
list => [__PACKAGE__, 'index', ['storage'], { node => $nodename }, $print_list],
remove => [__PACKAGE__, 'remove', ['template_path'], { node => $nodename }],
};
1;

View file

@ -45,69 +45,70 @@ sub setup_environment {
PVE::RPCEnvironment->setup_default_cli_env();
}
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'purge',
path => 'purge',
method => 'POST',
description => "Destroy ceph related data and configuration files.",
parameters => {
additionalProperties => 0,
properties => {
logs => {
description => 'Additionally purge Ceph logs, /var/log/ceph.',
type => 'boolean',
optional => 1,
},
crash => {
description => 'Additionally purge Ceph crash logs, /var/lib/ceph/crash.',
type => 'boolean',
optional => 1,
},
},
additionalProperties => 0,
properties => {
logs => {
description => 'Additionally purge Ceph logs, /var/log/ceph.',
type => 'boolean',
optional => 1,
},
crash => {
description => 'Additionally purge Ceph crash logs, /var/lib/ceph/crash.',
type => 'boolean',
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $message;
my $pools = [];
my $monstat = {};
my $mdsstat = {};
my $osdstat = [];
my $message;
my $pools = [];
my $monstat = {};
my $mdsstat = {};
my $osdstat = [];
eval {
my $rados = PVE::RADOS->new();
$pools = PVE::Ceph::Tools::ls_pools(undef, $rados);
$monstat = PVE::Ceph::Services::get_services_info('mon', undef, $rados);
$mdsstat = PVE::Ceph::Services::get_services_info('mds', undef, $rados);
$osdstat = $rados->mon_command({ prefix => 'osd metadata' });
};
warn "Error gathering ceph info, already purged? Message: $@" if $@;
eval {
my $rados = PVE::RADOS->new();
$pools = PVE::Ceph::Tools::ls_pools(undef, $rados);
$monstat = PVE::Ceph::Services::get_services_info('mon', undef, $rados);
$mdsstat = PVE::Ceph::Services::get_services_info('mds', undef, $rados);
$osdstat = $rados->mon_command({ prefix => 'osd metadata' });
};
warn "Error gathering ceph info, already purged? Message: $@" if $@;
my $osd = grep { $_->{hostname} eq $nodename } @$osdstat;
my $mds = grep { $mdsstat->{$_}->{host} eq $nodename } keys %$mdsstat;
my $mon = grep { $monstat->{$_}->{host} eq $nodename } keys %$monstat;
my $osd = grep { $_->{hostname} eq $nodename } @$osdstat;
my $mds = grep { $mdsstat->{$_}->{host} eq $nodename } keys %$mdsstat;
my $mon = grep { $monstat->{$_}->{host} eq $nodename } keys %$monstat;
# no pools = no data
$message .= "- remove pools, this will !!DESTROY DATA!!\n" if @$pools;
$message .= "- remove active OSD on $nodename\n" if $osd;
$message .= "- remove active MDS on $nodename\n" if $mds;
$message .= "- remove other MONs, $nodename is not the last MON\n"
if scalar(keys %$monstat) > 1 && $mon;
# no pools = no data
$message .= "- remove pools, this will !!DESTROY DATA!!\n" if @$pools;
$message .= "- remove active OSD on $nodename\n" if $osd;
$message .= "- remove active MDS on $nodename\n" if $mds;
$message .= "- remove other MONs, $nodename is not the last MON\n"
if scalar(keys %$monstat) > 1 && $mon;
# display all steps at once
die "Unable to purge Ceph!\n\nTo continue:\n$message" if $message;
# display all steps at once
die "Unable to purge Ceph!\n\nTo continue:\n$message" if $message;
my $services = PVE::Ceph::Services::get_local_services();
$services->{mon} = $monstat if $mon;
$services->{crash}->{$nodename} = { direxists => 1 } if $param->{crash};
$services->{logs}->{$nodename} = { direxists => 1 } if $param->{logs};
my $services = PVE::Ceph::Services::get_local_services();
$services->{mon} = $monstat if $mon;
$services->{crash}->{$nodename} = { direxists => 1 } if $param->{crash};
$services->{logs}->{$nodename} = { direxists => 1 } if $param->{logs};
PVE::Ceph::Tools::purge_all_ceph_services($services);
PVE::Ceph::Tools::purge_all_ceph_files($services);
PVE::Ceph::Tools::purge_all_ceph_services($services);
PVE::Ceph::Tools::purge_all_ceph_files($services);
return undef;
}});
return undef;
},
});
my sub has_valid_subscription {
my $info = eval { Proxmox::RS::Subscription::read_subscription('/etc/subscription') } // {};
@ -118,155 +119,168 @@ my sub has_valid_subscription {
my $available_ceph_release_codenames = PVE::Ceph::Releases::get_available_ceph_release_codenames(1);
my $default_ceph_version = PVE::Ceph::Releases::get_default_ceph_release_codename();
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'install',
path => 'install',
method => 'POST',
description => "Install ceph related packages.",
parameters => {
additionalProperties => 0,
properties => {
version => {
type => 'string',
enum => $available_ceph_release_codenames,
default => $default_ceph_version,
description => "Ceph version to install.",
optional => 1,
},
repository => {
type => 'string',
enum => ['enterprise', 'no-subscription', 'test'],
default => 'enterprise',
description => "Ceph repository to use.",
optional => 1,
},
'allow-experimental' => {
type => 'boolean',
default => 0,
optional => 1,
description => "Allow experimental versions. Use with care!",
},
},
additionalProperties => 0,
properties => {
version => {
type => 'string',
enum => $available_ceph_release_codenames,
default => $default_ceph_version,
description => "Ceph version to install.",
optional => 1,
},
repository => {
type => 'string',
enum => ['enterprise', 'no-subscription', 'test'],
default => 'enterprise',
description => "Ceph repository to use.",
optional => 1,
},
'allow-experimental' => {
type => 'boolean',
default => 0,
optional => 1,
description => "Allow experimental versions. Use with care!",
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $cephver = $param->{version} || $default_ceph_version;
my $cephver = $param->{version} || $default_ceph_version;
my $repo = $param->{'repository'} // 'enterprise';
my $enterprise_repo = $repo eq 'enterprise';
my $cdn = $enterprise_repo ? 'https://enterprise.proxmox.com' : 'http://download.proxmox.com';
my $repo = $param->{'repository'} // 'enterprise';
my $enterprise_repo = $repo eq 'enterprise';
my $cdn =
$enterprise_repo ? 'https://enterprise.proxmox.com' : 'http://download.proxmox.com';
if (has_valid_subscription()) {
warn "\nNOTE: The node has an active subscription but a non-production Ceph repository selected.\n\n"
if !$enterprise_repo;
} elsif ($enterprise_repo) {
warn "\nWARN: Enterprise repository selected, but no active subscription!\n\n";
} elsif ($repo eq 'no-subscription') {
warn "\nHINT: The no-subscription repository is not the best choice for production setups.\n"
."Proxmox recommends using the enterprise repository with a valid subscription.\n";
} else {
warn "\nWARN: The test repository should only be used for test setups or after consulting"
." the official Proxmox support!\n\n"
}
if (has_valid_subscription()) {
warn
"\nNOTE: The node has an active subscription but a non-production Ceph repository selected.\n\n"
if !$enterprise_repo;
} elsif ($enterprise_repo) {
warn "\nWARN: Enterprise repository selected, but no active subscription!\n\n";
} elsif ($repo eq 'no-subscription') {
warn
"\nHINT: The no-subscription repository is not the best choice for production setups.\n"
. "Proxmox recommends using the enterprise repository with a valid subscription.\n";
} else {
warn
"\nWARN: The test repository should only be used for test setups or after consulting"
. " the official Proxmox support!\n\n";
}
my $available_ceph_releases = PVE::Ceph::Releases::get_all_available_ceph_releases();
die "unsupported ceph version: $cephver" if !exists($available_ceph_releases->{$cephver});
my $available_ceph_releases = PVE::Ceph::Releases::get_all_available_ceph_releases();
die "unsupported ceph version: $cephver"
if !exists($available_ceph_releases->{$cephver});
my $repolist = "deb ${cdn}/debian/ceph-${cephver} bookworm $repo\n";
my $repolist = "deb ${cdn}/debian/ceph-${cephver} bookworm $repo\n";
my $rendered_release = $available_ceph_releases->{$cephver}->{release} . ' ' . ucfirst($cephver);
if (-t STDOUT && !$param->{version}) {
print "This will install Ceph ${rendered_release} - continue (y/N)? ";
my $rendered_release =
$available_ceph_releases->{$cephver}->{release} . ' ' . ucfirst($cephver);
if (-t STDOUT && !$param->{version}) {
print "This will install Ceph ${rendered_release} - continue (y/N)? ";
my $answer = <STDIN>;
my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
my $answer = <STDIN>;
my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
die "Aborting installation as requested\n" if !$continue;
}
die "Aborting installation as requested\n" if !$continue;
}
PVE::Tools::file_set_contents("/etc/apt/sources.list.d/ceph.list", $repolist);
PVE::Tools::file_set_contents("/etc/apt/sources.list.d/ceph.list", $repolist);
if ($available_ceph_releases->{$cephver}->{unsupported}) {
if ($param->{'allow-experimental'}) {
warn "NOTE: installing experimental/tech-preview Ceph release ${rendered_release}!\n";
} elsif (-t STDOUT) {
print "Ceph ${rendered_release} is currently considered a technology preview for Proxmox VE - continue (y/N)? ";
my $answer = <STDIN>;
my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
if ($available_ceph_releases->{$cephver}->{unsupported}) {
if ($param->{'allow-experimental'}) {
warn
"NOTE: installing experimental/tech-preview Ceph release ${rendered_release}!\n";
} elsif (-t STDOUT) {
print
"Ceph ${rendered_release} is currently considered a technology preview for Proxmox VE - continue (y/N)? ";
my $answer = <STDIN>;
my $continue = defined($answer) && $answer =~ m/^\s*y(?:es)?\s*$/i;
die "Aborting installation as requested\n" if !$continue;
} else {
die "refusing to install tech-preview Ceph release ${rendered_release} without 'allow-experimental' parameter!\n";
}
}
die "Aborting installation as requested\n" if !$continue;
} else {
die
"refusing to install tech-preview Ceph release ${rendered_release} without 'allow-experimental' parameter!\n";
}
}
local $ENV{DEBIAN_FRONTEND} = 'noninteractive';
print "update available package list\n";
eval {
run_command(
['apt-get', '-q', 'update'],
outfunc => sub {},
errfunc => sub { print STDERR "$_[0]\n" },
)
};
local $ENV{DEBIAN_FRONTEND} = 'noninteractive';
print "update available package list\n";
eval {
run_command(
['apt-get', '-q', 'update'],
outfunc => sub { },
errfunc => sub { print STDERR "$_[0]\n" },
);
};
my @apt_install = qw(apt-get --no-install-recommends -o Dpkg::Options::=--force-confnew install --);
my @ceph_packages = qw(
ceph
ceph-common
ceph-fuse
ceph-mds
ceph-volume
gdisk
nvme-cli
);
my @apt_install =
qw(apt-get --no-install-recommends -o Dpkg::Options::=--force-confnew install --);
my @ceph_packages = qw(
ceph
ceph-common
ceph-fuse
ceph-mds
ceph-volume
gdisk
nvme-cli
);
print "start installation\n";
print "start installation\n";
# this flag helps to determine when apt is actually done installing (vs. partial extracting)
my $install_flag_fn = PVE::Ceph::Tools::ceph_install_flag_file();
open(my $install_flag, '>', $install_flag_fn) or die "could not create install flag - $!\n";
close $install_flag;
# this flag helps to determine when apt is actually done installing (vs. partial extracting)
my $install_flag_fn = PVE::Ceph::Tools::ceph_install_flag_file();
open(my $install_flag, '>', $install_flag_fn)
or die "could not create install flag - $!\n";
close $install_flag;
if (system(@apt_install, @ceph_packages) != 0) {
unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
die "apt failed during ceph installation ($?)\n";
}
if (system(@apt_install, @ceph_packages) != 0) {
unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
die "apt failed during ceph installation ($?)\n";
}
print "\ninstalled Ceph ${rendered_release} successfully!\n";
# done: drop flag file so that the PVE::Ceph::Tools check returns Ok now.
unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
print "\ninstalled Ceph ${rendered_release} successfully!\n";
# done: drop flag file so that the PVE::Ceph::Tools check returns Ok now.
unlink $install_flag_fn or warn "could not remove Ceph installation flag - $!";
print "\nreloading API to load new Ceph RADOS library...\n";
run_command([
'systemctl', 'try-reload-or-restart', 'pvedaemon.service', 'pveproxy.service'
]);
print "\nreloading API to load new Ceph RADOS library...\n";
run_command([
'systemctl', 'try-reload-or-restart', 'pvedaemon.service', 'pveproxy.service',
]);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'status',
path => 'status',
method => 'GET',
description => "Get Ceph Status.",
parameters => {
additionalProperties => 0,
additionalProperties => 0,
},
returns => { type => 'null' },
code => sub {
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
run_command(
['ceph', '-s'],
outfunc => sub { print "$_[0]\n" },
errfunc => sub { print STDERR "$_[0]\n" },
timeout => 15,
);
return undef;
}});
run_command(
['ceph', '-s'],
outfunc => sub { print "$_[0]\n" },
errfunc => sub { print STDERR "$_[0]\n" },
timeout => 15,
);
return undef;
},
});
my $get_storages = sub {
my ($fs, $is_default) = @_;
@ -276,159 +290,161 @@ my $get_storages = sub {
my $storages = $cfg->{ids};
my $res = {};
foreach my $storeid (keys %$storages) {
my $curr = $storages->{$storeid};
next if $curr->{type} ne 'cephfs';
my $cur_fs = $curr->{'fs-name'};
$res->{$storeid} = $storages->{$storeid}
if (!defined($cur_fs) && $is_default) || (defined($cur_fs) && $fs eq $cur_fs);
my $curr = $storages->{$storeid};
next if $curr->{type} ne 'cephfs';
my $cur_fs = $curr->{'fs-name'};
$res->{$storeid} = $storages->{$storeid}
if (!defined($cur_fs) && $is_default) || (defined($cur_fs) && $fs eq $cur_fs);
}
return $res;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'destroyfs',
path => 'destroyfs',
method => 'DELETE',
description => "Destroy a Ceph filesystem",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => "The ceph filesystem name.",
type => 'string',
},
'remove-storages' => {
description => "Remove all pveceph-managed storages configured for this fs.",
type => 'boolean',
optional => 1,
default => 0,
},
'remove-pools' => {
description => "Remove data and metadata pools configured for this fs.",
type => 'boolean',
optional => 1,
default => 0,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
name => {
description => "The ceph filesystem name.",
type => 'string',
},
'remove-storages' => {
description => "Remove all pveceph-managed storages configured for this fs.",
type => 'boolean',
optional => 1,
default => 0,
},
'remove-pools' => {
description => "Remove data and metadata pools configured for this fs.",
type => 'boolean',
optional => 1,
default => 0,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $rpcenv = PVE::RPCEnvironment::get();
my $user = $rpcenv->get_user();
my $fs_name = $param->{name};
my $fs_name = $param->{name};
my $fs;
my $fs_list = PVE::Ceph::Tools::ls_fs();
for my $entry (@$fs_list) {
next if $entry->{name} ne $fs_name;
$fs = $entry;
last;
}
die "no such cephfs '$fs_name'\n" if !$fs;
my $fs;
my $fs_list = PVE::Ceph::Tools::ls_fs();
for my $entry (@$fs_list) {
next if $entry->{name} ne $fs_name;
$fs = $entry;
last;
}
die "no such cephfs '$fs_name'\n" if !$fs;
my $worker = sub {
my $rados = PVE::RADOS->new();
my $worker = sub {
my $rados = PVE::RADOS->new();
if ($param->{'remove-storages'}) {
my $defaultfs;
my $fs_dump = $rados->mon_command({ prefix => "fs dump" });
for my $fs ($fs_dump->{filesystems}->@*) {
next if $fs->{id} != $fs_dump->{default_fscid};
$defaultfs = $fs->{mdsmap}->{fs_name};
}
warn "no default fs found, maybe not all relevant storages are removed\n"
if !defined($defaultfs);
if ($param->{'remove-storages'}) {
my $defaultfs;
my $fs_dump = $rados->mon_command({ prefix => "fs dump" });
for my $fs ($fs_dump->{filesystems}->@*) {
next if $fs->{id} != $fs_dump->{default_fscid};
$defaultfs = $fs->{mdsmap}->{fs_name};
}
warn "no default fs found, maybe not all relevant storages are removed\n"
if !defined($defaultfs);
my $storages = $get_storages->($fs_name, $fs_name eq ($defaultfs // ''));
for my $storeid (keys %$storages) {
my $store = $storages->{$storeid};
if (!$store->{disable}) {
die "storage '$storeid' is not disabled, make sure to disable ".
"and unmount the storage first\n";
}
}
my $storages = $get_storages->($fs_name, $fs_name eq ($defaultfs // ''));
for my $storeid (keys %$storages) {
my $store = $storages->{$storeid};
if (!$store->{disable}) {
die "storage '$storeid' is not disabled, make sure to disable "
. "and unmount the storage first\n";
}
}
my $err;
for my $storeid (keys %$storages) {
# skip external clusters, not managed by pveceph
next if $storages->{$storeid}->{monhost};
eval { PVE::API2::Storage::Config->delete({storage => $storeid}) };
if ($@) {
warn "failed to remove storage '$storeid': $@\n";
$err = 1;
}
}
die "failed to remove (some) storages - check log and remove manually!\n"
if $err;
}
my $err;
for my $storeid (keys %$storages) {
# skip external clusters, not managed by pveceph
next if $storages->{$storeid}->{monhost};
eval { PVE::API2::Storage::Config->delete({ storage => $storeid }) };
if ($@) {
warn "failed to remove storage '$storeid': $@\n";
$err = 1;
}
}
die "failed to remove (some) storages - check log and remove manually!\n"
if $err;
}
PVE::Ceph::Tools::destroy_fs($fs_name, $rados);
PVE::Ceph::Tools::destroy_fs($fs_name, $rados);
if ($param->{'remove-pools'}) {
warn "removing metadata pool '$fs->{metadata_pool}'\n";
eval { PVE::Ceph::Tools::destroy_pool($fs->{metadata_pool}, $rados) };
warn "$@\n" if $@;
if ($param->{'remove-pools'}) {
warn "removing metadata pool '$fs->{metadata_pool}'\n";
eval { PVE::Ceph::Tools::destroy_pool($fs->{metadata_pool}, $rados) };
warn "$@\n" if $@;
foreach my $pool ($fs->{data_pools}->@*) {
warn "removing data pool '$pool'\n";
eval { PVE::Ceph::Tools::destroy_pool($pool, $rados) };
warn "$@\n" if $@;
}
}
foreach my $pool ($fs->{data_pools}->@*) {
warn "removing data pool '$pool'\n";
eval { PVE::Ceph::Tools::destroy_pool($pool, $rados) };
warn "$@\n" if $@;
}
}
};
return $rpcenv->fork_worker('cephdestroyfs', $fs_name, $user, $worker);
}});
};
return $rpcenv->fork_worker('cephdestroyfs', $fs_name, $user, $worker);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'osd-details',
path => 'osd-details',
method => 'GET',
description => "Get OSD details.",
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
osdid => {
description => "ID of the OSD",
type => 'string',
},
verbose => {
description => "Print verbose information, same as json-pretty output format.",
type => 'boolean',
default => 0,
optional => 1,
},
},
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
osdid => {
description => "ID of the OSD",
type => 'string',
},
verbose => {
description => "Print verbose information, same as json-pretty output format.",
type => 'boolean',
default => 0,
optional => 1,
},
},
},
returns => { type => 'object' },
code => sub {
my ($param) = @_;
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::check_ceph_inited();
my $res = PVE::API2::Ceph::OSD->osddetails({
osdid => $param->{osdid},
node => $param->{node},
});
my $res = PVE::API2::Ceph::OSD->osddetails({
osdid => $param->{osdid},
node => $param->{node},
});
for my $dev ($res->{devices}->@*) {
$dev->{"lv-info"} = PVE::API2::Ceph::OSD->osdvolume({
osdid => $param->{osdid},
node => $param->{node},
type => $dev->{device},
});
}
$res->{verbose} = 1 if $param->{verbose};
return $res;
}});
for my $dev ($res->{devices}->@*) {
$dev->{"lv-info"} = PVE::API2::Ceph::OSD->osdvolume({
osdid => $param->{osdid},
node => $param->{node},
type => $dev->{device},
});
}
$res->{verbose} = 1 if $param->{verbose};
return $res;
},
});
my $format_osddetails = sub {
my ($data, $schema, $options) = @_;
@ -436,91 +452,118 @@ my $format_osddetails = sub {
$options->{"output-format"} //= "text";
if ($data->{verbose}) {
$options->{"output-format"} = "json-pretty";
delete $data->{verbose};
$options->{"output-format"} = "json-pretty";
delete $data->{verbose};
}
if ($options->{"output-format"} eq "text") {
for my $dev ($data->{devices}->@*) {
my ($disk, $type, $device) = $dev->@{'physical_device', 'type', 'device'};
my ($lv_size, $lv_ctime) = $dev->{'lv-info'}->@{'lv_size', 'creation_time'};
for my $dev ($data->{devices}->@*) {
my ($disk, $type, $device) = $dev->@{ 'physical_device', 'type', 'device' };
my ($lv_size, $lv_ctime) = $dev->{'lv-info'}->@{ 'lv_size', 'creation_time' };
$data->{osd}->{$device} = "Disk: $disk, Type: $type, LV Size: $lv_size, LV Creation Time: $lv_ctime";
}
PVE::CLIFormatter::print_api_result($data->{osd}, $schema, undef, $options);
$data->{osd}->{$device} =
"Disk: $disk, Type: $type, LV Size: $lv_size, LV Creation Time: $lv_ctime";
}
PVE::CLIFormatter::print_api_result($data->{osd}, $schema, undef, $options);
} else {
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}
};
our $cmddef = {
init => [ 'PVE::API2::Ceph', 'init', [], { node => $nodename } ],
init => ['PVE::API2::Ceph', 'init', [], { node => $nodename }],
pool => {
ls => [ 'PVE::API2::Ceph::Pool', 'lspools', [], { node => $nodename }, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema,
[
'pool_name',
'size',
'min_size',
'pg_num',
'pg_num_min',
'pg_num_final',
'pg_autoscale_mode',
'target_size',
'target_size_ratio',
'crush_rule_name',
'percent_used',
'bytes_used',
],
$options);
}, $PVE::RESTHandler::standard_output_options],
create => [ 'PVE::API2::Ceph::Pool', 'createpool', ['name'], { node => $nodename }],
destroy => [ 'PVE::API2::Ceph::Pool', 'destroypool', ['name'], { node => $nodename } ],
set => [ 'PVE::API2::Ceph::Pool', 'setpool', ['name'], { node => $nodename } ],
get => [ 'PVE::API2::Ceph::Pool', 'getpool', ['name'], { node => $nodename }, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}, $PVE::RESTHandler::standard_output_options],
ls => [
'PVE::API2::Ceph::Pool',
'lspools',
[],
{ node => $nodename },
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result(
$data,
$schema,
[
'pool_name',
'size',
'min_size',
'pg_num',
'pg_num_min',
'pg_num_final',
'pg_autoscale_mode',
'target_size',
'target_size_ratio',
'crush_rule_name',
'percent_used',
'bytes_used',
],
$options,
);
},
$PVE::RESTHandler::standard_output_options,
],
create => ['PVE::API2::Ceph::Pool', 'createpool', ['name'], { node => $nodename }],
destroy => ['PVE::API2::Ceph::Pool', 'destroypool', ['name'], { node => $nodename }],
set => ['PVE::API2::Ceph::Pool', 'setpool', ['name'], { node => $nodename }],
get => [
'PVE::API2::Ceph::Pool',
'getpool',
['name'],
{ node => $nodename },
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
},
$PVE::RESTHandler::standard_output_options,
],
},
lspools => { alias => 'pool ls' },
createpool => { alias => 'pool create' },
destroypool => { alias => 'pool destroy' },
fs => {
create => [ 'PVE::API2::Ceph::FS', 'createfs', [], { node => $nodename }],
destroy => [ __PACKAGE__, 'destroyfs', ['name'], { node => $nodename }],
create => ['PVE::API2::Ceph::FS', 'createfs', [], { node => $nodename }],
destroy => [__PACKAGE__, 'destroyfs', ['name'], { node => $nodename }],
},
osd => {
create => [ 'PVE::API2::Ceph::OSD', 'createosd', ['dev'], { node => $nodename }, $upid_exit],
destroy => [ 'PVE::API2::Ceph::OSD', 'destroyosd', ['osdid'], { node => $nodename }, $upid_exit],
details => [
__PACKAGE__, 'osd-details', ['osdid'], { node => $nodename }, $format_osddetails,
$PVE::RESTHandler::standard_output_options,
],
create =>
['PVE::API2::Ceph::OSD', 'createosd', ['dev'], { node => $nodename }, $upid_exit],
destroy =>
['PVE::API2::Ceph::OSD', 'destroyosd', ['osdid'], { node => $nodename }, $upid_exit],
details => [
__PACKAGE__,
'osd-details',
['osdid'],
{ node => $nodename },
$format_osddetails,
$PVE::RESTHandler::standard_output_options,
],
},
createosd => { alias => 'osd create' },
destroyosd => { alias => 'osd destroy' },
mon => {
create => [ 'PVE::API2::Ceph::MON', 'createmon', [], { node => $nodename }, $upid_exit],
destroy => [ 'PVE::API2::Ceph::MON', 'destroymon', ['monid'], { node => $nodename }, $upid_exit],
create => ['PVE::API2::Ceph::MON', 'createmon', [], { node => $nodename }, $upid_exit],
destroy =>
['PVE::API2::Ceph::MON', 'destroymon', ['monid'], { node => $nodename }, $upid_exit],
},
createmon => { alias => 'mon create' },
destroymon => { alias => 'mon destroy' },
mgr => {
create => [ 'PVE::API2::Ceph::MGR', 'createmgr', [], { node => $nodename }, $upid_exit],
destroy => [ 'PVE::API2::Ceph::MGR', 'destroymgr', ['id'], { node => $nodename }, $upid_exit],
create => ['PVE::API2::Ceph::MGR', 'createmgr', [], { node => $nodename }, $upid_exit],
destroy =>
['PVE::API2::Ceph::MGR', 'destroymgr', ['id'], { node => $nodename }, $upid_exit],
},
createmgr => { alias => 'mgr create' },
destroymgr => { alias => 'mgr destroy' },
mds => {
create => [ 'PVE::API2::Ceph::MDS', 'createmds', [], { node => $nodename }, $upid_exit],
destroy => [ 'PVE::API2::Ceph::MDS', 'destroymds', ['name'], { node => $nodename }, $upid_exit],
create => ['PVE::API2::Ceph::MDS', 'createmds', [], { node => $nodename }, $upid_exit],
destroy =>
['PVE::API2::Ceph::MDS', 'destroymds', ['name'], { node => $nodename }, $upid_exit],
},
start => [ 'PVE::API2::Ceph', 'start', [], { node => $nodename }, $upid_exit],
stop => [ 'PVE::API2::Ceph', 'stop', [], { node => $nodename }, $upid_exit],
install => [ __PACKAGE__, 'install', [] ],
purge => [ __PACKAGE__, 'purge', [] ],
status => [ __PACKAGE__, 'status', []],
start => ['PVE::API2::Ceph', 'start', [], { node => $nodename }, $upid_exit],
stop => ['PVE::API2::Ceph', 'stop', [], { node => $nodename }, $upid_exit],
install => [__PACKAGE__, 'install', []],
purge => [__PACKAGE__, 'purge', []],
status => [__PACKAGE__, 'status', []],
};
1;

View file

@ -43,22 +43,35 @@ sub param_mapping {
my ($name) = @_;
my $load_file_and_encode = sub {
my ($filename) = @_;
my ($filename) = @_;
return PVE::ACME::Challenge->encode_value('string', 'data', PVE::Tools::file_get_contents($filename));
return PVE::ACME::Challenge->encode_value(
'string',
'data',
PVE::Tools::file_get_contents($filename),
);
};
my $mapping = {
'upload_custom_cert' => [
'certificates',
'key',
],
'add_plugin' => [
['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
],
'update_plugin' => [
['data', $load_file_and_encode, "File with one key-value pair per line, will be base64url encode for storage in plugin config.", 0],
],
'upload_custom_cert' => [
'certificates', 'key',
],
'add_plugin' => [
[
'data',
$load_file_and_encode,
"File with one key-value pair per line, will be base64url encode for storage in plugin config.",
0,
],
],
'update_plugin' => [
[
'data',
$load_file_and_encode,
"File with one key-value pair per line, will be base64url encode for storage in plugin config.",
0,
],
],
};
return $mapping->{$name};
@ -70,203 +83,297 @@ __PACKAGE__->register_method({
method => 'POST',
description => "Register a new ACME account with a compatible CA.",
parameters => {
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact'),
directory => get_standard_option('pve-acme-directory-url', {
optional => 1,
}),
},
additionalProperties => 0,
properties => {
name => get_standard_option('pve-acme-account-name'),
contact => get_standard_option('pve-acme-account-contact'),
directory => get_standard_option('pve-acme-directory-url', {
optional => 1,
}),
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $custom_directory = 0;
if (!$param->{directory}) {
my $directories = PVE::API2::ACMEAccount->get_directories({});
print "Directory endpoints:\n";
my $i = 0;
while ($i < @$directories) {
print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url}, ")\n";
$i++;
}
print $i, ") Custom\n";
my $custom_directory = 0;
if (!$param->{directory}) {
my $directories = PVE::API2::ACMEAccount->get_directories({});
print "Directory endpoints:\n";
my $i = 0;
while ($i < @$directories) {
print $i, ") ", $directories->[$i]->{name}, " (", $directories->[$i]->{url},
")\n";
$i++;
}
print $i, ") Custom\n";
my $term = Term::ReadLine->new('pvenode');
my $get_dir_selection = sub {
my $selection = $term->readline("Enter selection: ");
if ($selection =~ /^(\d+)$/) {
$selection = $1;
if ($selection == $i) {
$param->{directory} = $term->readline("Enter custom URL: ");
$custom_directory = 1;
return;
} elsif ($selection < $i && $selection >= 0) {
$param->{directory} = $directories->[$selection]->{url};
return;
}
}
print "Invalid selection.\n";
};
my $term = Term::ReadLine->new('pvenode');
my $get_dir_selection = sub {
my $selection = $term->readline("Enter selection: ");
if ($selection =~ /^(\d+)$/) {
$selection = $1;
if ($selection == $i) {
$param->{directory} = $term->readline("Enter custom URL: ");
$custom_directory = 1;
return;
} elsif ($selection < $i && $selection >= 0) {
$param->{directory} = $directories->[$selection]->{url};
return;
}
}
print "Invalid selection.\n";
};
my $attempts = 0;
while (!$param->{directory}) {
die "Aborting.\n" if $attempts > 3;
$get_dir_selection->();
$attempts++;
}
}
print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n";
my $meta = PVE::API2::ACMEAccount->get_meta({ directory => $param->{directory} });
if ($meta->{termsOfService}) {
my $tos = $meta->{termsOfService};
print "Terms of Service: $tos\n";
my $term = Term::ReadLine->new('pvenode');
my $agreed = $term->readline('Do you agree to the above terms? [y|N]: ');
die "Cannot continue without agreeing to ToS, aborting.\n"
if ($agreed !~ /^y$/i);
my $attempts = 0;
while (!$param->{directory}) {
die "Aborting.\n" if $attempts > 3;
$get_dir_selection->();
$attempts++;
}
}
print "\nAttempting to fetch Terms of Service from '$param->{directory}'..\n";
my $meta = PVE::API2::ACMEAccount->get_meta({ directory => $param->{directory} });
if ($meta->{termsOfService}) {
my $tos = $meta->{termsOfService};
print "Terms of Service: $tos\n";
my $term = Term::ReadLine->new('pvenode');
my $agreed = $term->readline('Do you agree to the above terms? [y|N]: ');
die "Cannot continue without agreeing to ToS, aborting.\n"
if ($agreed !~ /^y$/i);
$param->{tos_url} = $tos;
} else {
print "No Terms of Service found, proceeding.\n";
}
$param->{tos_url} = $tos;
} else {
print "No Terms of Service found, proceeding.\n";
}
my $eab_enabled = $meta->{externalAccountRequired};
if (!$eab_enabled && $custom_directory) {
my $term = Term::ReadLine->new('pvenode');
my $agreed = $term->readline('Do you want to use external account binding? [y|N]: ');
$eab_enabled = ($agreed =~ /^y$/i);
} elsif ($eab_enabled) {
print "The CA requires external account binding.\n";
}
if ($eab_enabled) {
print "You should have received a key id and a key from your CA.\n";
my $term = Term::ReadLine->new('pvenode');
my $eab_kid = $term->readline('Enter EAB key id: ');
my $eab_hmac_key = $term->readline('Enter EAB key: ');
my $eab_enabled = $meta->{externalAccountRequired};
if (!$eab_enabled && $custom_directory) {
my $term = Term::ReadLine->new('pvenode');
my $agreed =
$term->readline('Do you want to use external account binding? [y|N]: ');
$eab_enabled = ($agreed =~ /^y$/i);
} elsif ($eab_enabled) {
print "The CA requires external account binding.\n";
}
if ($eab_enabled) {
print "You should have received a key id and a key from your CA.\n";
my $term = Term::ReadLine->new('pvenode');
my $eab_kid = $term->readline('Enter EAB key id: ');
my $eab_hmac_key = $term->readline('Enter EAB key: ');
$param->{'eab-kid'} = $eab_kid;
$param->{'eab-hmac-key'} = $eab_hmac_key;
}
$param->{'eab-kid'} = $eab_kid;
$param->{'eab-hmac-key'} = $eab_hmac_key;
}
print "\nAttempting to register account with '$param->{directory}'..\n";
print "\nAttempting to register account with '$param->{directory}'..\n";
$upid_exit->(PVE::API2::ACMEAccount->register_account($param));
}});
$upid_exit->(PVE::API2::ACMEAccount->register_account($param));
},
});
my $print_cert_info = sub {
my ($schema, $cert, $options) = @_;
my $order = [qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)];
my $order = [
qw(filename fingerprint subject issuer notbefore notafter public-key-type public-key-bits san)
];
PVE::CLIFormatter::print_api_result(
$cert, $schema, $order, { %$options, noheader => 1, sort_key => 0 });
$cert,
$schema,
$order,
{ %$options, noheader => 1, sort_key => 0 },
);
};
our $cmddef = {
config => {
get => [ 'PVE::API2::NodeConfig', 'get_config', [], { node => $nodename }, sub {
my ($res) = @_;
print PVE::NodeConfig::write_node_config($res);
}],
set => [ 'PVE::API2::NodeConfig', 'set_options', [], { node => $nodename } ],
get => [
'PVE::API2::NodeConfig',
'get_config',
[],
{ node => $nodename },
sub {
my ($res) = @_;
print PVE::NodeConfig::write_node_config($res);
},
],
set => ['PVE::API2::NodeConfig', 'set_options', [], { node => $nodename }],
},
startall => [ 'PVE::API2::Nodes::Nodeinfo', 'startall', [], { node => $nodename } ],
stopall => [ 'PVE::API2::Nodes::Nodeinfo', 'stopall', [], { node => $nodename } ],
migrateall => [ 'PVE::API2::Nodes::Nodeinfo', 'migrateall', [ 'target' ], { node => $nodename } ],
startall => ['PVE::API2::Nodes::Nodeinfo', 'startall', [], { node => $nodename }],
stopall => ['PVE::API2::Nodes::Nodeinfo', 'stopall', [], { node => $nodename }],
migrateall =>
['PVE::API2::Nodes::Nodeinfo', 'migrateall', ['target'], { node => $nodename }],
cert => {
info => [ 'PVE::API2::Certificates', 'info', [], { node => $nodename }, sub {
my ($res, $schema, $options) = @_;
info => [
'PVE::API2::Certificates',
'info',
[],
{ node => $nodename },
sub {
my ($res, $schema, $options) = @_;
if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') {
for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) {
$print_cert_info->($schema->{items}, $cert, $options);
}
} else {
PVE::CLIFormatter::print_api_result($res, $schema, undef, $options);
}
if (!$options->{'output-format'} || $options->{'output-format'} eq 'text') {
for my $cert (sort { $a->{filename} cmp $b->{filename} } @$res) {
$print_cert_info->($schema->{items}, $cert, $options);
}
} else {
PVE::CLIFormatter::print_api_result($res, $schema, undef, $options);
}
}, $PVE::RESTHandler::standard_output_options],
set => [ 'PVE::API2::Certificates', 'upload_custom_cert', ['certificates', 'key'], { node => $nodename }, sub {
my ($res, $schema, $options) = @_;
$print_cert_info->($schema, $res, $options);
}, $PVE::RESTHandler::standard_output_options],
delete => [ 'PVE::API2::Certificates', 'remove_custom_cert', ['restart'], { node => $nodename } ],
},
$PVE::RESTHandler::standard_output_options,
],
set => [
'PVE::API2::Certificates',
'upload_custom_cert',
['certificates', 'key'],
{ node => $nodename },
sub {
my ($res, $schema, $options) = @_;
$print_cert_info->($schema, $res, $options);
},
$PVE::RESTHandler::standard_output_options,
],
delete =>
['PVE::API2::Certificates', 'remove_custom_cert', ['restart'], { node => $nodename }],
},
task => {
list => [ 'PVE::API2::Tasks', 'node_tasks', [], { node => $nodename }, sub {
my ($data, $schema, $options) = @_;
foreach my $task (@$data) {
if (!defined($task->{status})) {
$task->{status} = 'UNKNOWN';
# RUNNING is set by the API call and needs to be checked explicitly
} elsif (PVE::Tools::upid_status_is_error($task->{status}) &&
$task->{status} ne 'RUNNING')
{
$task->{status} = 'ERROR';
}
}
PVE::CLIFormatter::print_api_result($data, $schema, ['upid', 'type', 'id', 'user', 'starttime', 'endtime', 'status' ], $options);
}, $PVE::RESTHandler::standard_output_options],
status => [ 'PVE::API2::Tasks', 'read_task_status', [ 'upid' ], { node => $nodename }, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}, $PVE::RESTHandler::standard_output_options],
# set limit to 1000000, so we see the whole log, not only the first 50 lines by default
log => [ 'PVE::API2::Tasks', 'read_task_log', [ 'upid' ], { node => $nodename, limit => 1000000 }, sub {
my ($data, $resultprops) = @_;
foreach my $line (@$data) {
print $line->{t} . "\n";
}
}],
list => [
'PVE::API2::Tasks',
'node_tasks',
[],
{ node => $nodename },
sub {
my ($data, $schema, $options) = @_;
foreach my $task (@$data) {
if (!defined($task->{status})) {
$task->{status} = 'UNKNOWN';
# RUNNING is set by the API call and needs to be checked explicitly
} elsif (
PVE::Tools::upid_status_is_error($task->{status})
&& $task->{status} ne 'RUNNING'
) {
$task->{status} = 'ERROR';
}
}
PVE::CLIFormatter::print_api_result(
$data,
$schema,
['upid', 'type', 'id', 'user', 'starttime', 'endtime', 'status'],
$options,
);
},
$PVE::RESTHandler::standard_output_options,
],
status => [
'PVE::API2::Tasks',
'read_task_status',
['upid'],
{ node => $nodename },
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
},
$PVE::RESTHandler::standard_output_options,
],
# set limit to 1000000, so we see the whole log, not only the first 50 lines by default
log => [
'PVE::API2::Tasks',
'read_task_log',
['upid'],
{ node => $nodename, limit => 1000000 },
sub {
my ($data, $resultprops) = @_;
foreach my $line (@$data) {
print $line->{t} . "\n";
}
},
],
},
acme => {
account => {
list => [ 'PVE::API2::ACMEAccount', 'account_index', [], {}, sub {
my ($res) = @_;
for my $acc (@$res) {
print "$acc->{name}\n";
}
}],
register => [ __PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit ],
deactivate => [ 'PVE::API2::ACMEAccount', 'deactivate_account', ['name'], {}, $upid_exit ],
info => [ 'PVE::API2::ACMEAccount', 'get_account', ['name'], {}, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}, $PVE::RESTHandler::standard_output_options],
update => [ 'PVE::API2::ACMEAccount', 'update_account', ['name'], {}, $upid_exit ],
},
cert => {
order => [ 'PVE::API2::ACME', 'new_certificate', [], { node => $nodename }, $upid_exit ],
renew => [ 'PVE::API2::ACME', 'renew_certificate', [], { node => $nodename }, $upid_exit ],
revoke => [ 'PVE::API2::ACME', 'revoke_certificate', [], { node => $nodename }, $upid_exit ],
},
plugin => {
list => [ 'PVE::API2::ACMEPlugin', 'index', [], {}, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}, $PVE::RESTHandler::standard_output_options ],
config => [ 'PVE::API2::ACMEPlugin', 'get_plugin_config', ['id'], {}, sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
}, $PVE::RESTHandler::standard_output_options ],
add => [ 'PVE::API2::ACMEPlugin', 'add_plugin', ['type', 'id'] ],
set => [ 'PVE::API2::ACMEPlugin', 'update_plugin', ['id'] ],
remove => [ 'PVE::API2::ACMEPlugin', 'delete_plugin', ['id'] ],
},
account => {
list => [
'PVE::API2::ACMEAccount',
'account_index',
[],
{},
sub {
my ($res) = @_;
for my $acc (@$res) {
print "$acc->{name}\n";
}
},
],
register => [__PACKAGE__, 'acme_register', ['name', 'contact'], {}, $upid_exit],
deactivate =>
['PVE::API2::ACMEAccount', 'deactivate_account', ['name'], {}, $upid_exit],
info => [
'PVE::API2::ACMEAccount',
'get_account',
['name'],
{},
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
},
$PVE::RESTHandler::standard_output_options,
],
update => ['PVE::API2::ACMEAccount', 'update_account', ['name'], {}, $upid_exit],
},
cert => {
order =>
['PVE::API2::ACME', 'new_certificate', [], { node => $nodename }, $upid_exit],
renew =>
['PVE::API2::ACME', 'renew_certificate', [], { node => $nodename }, $upid_exit],
revoke =>
['PVE::API2::ACME', 'revoke_certificate', [], { node => $nodename }, $upid_exit],
},
plugin => {
list => [
'PVE::API2::ACMEPlugin',
'index',
[],
{},
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
},
$PVE::RESTHandler::standard_output_options,
],
config => [
'PVE::API2::ACMEPlugin',
'get_plugin_config',
['id'],
{},
sub {
my ($data, $schema, $options) = @_;
PVE::CLIFormatter::print_api_result($data, $schema, undef, $options);
},
$PVE::RESTHandler::standard_output_options,
],
add => ['PVE::API2::ACMEPlugin', 'add_plugin', ['type', 'id']],
set => ['PVE::API2::ACMEPlugin', 'update_plugin', ['id']],
remove => ['PVE::API2::ACMEPlugin', 'delete_plugin', ['id']],
},
},
wakeonlan => [ 'PVE::API2::Nodes::Nodeinfo', 'wakeonlan', [ 'node' ], {}, sub {
my ($mac_addr) = @_;
wakeonlan => [
'PVE::API2::Nodes::Nodeinfo',
'wakeonlan',
['node'],
{},
sub {
my ($mac_addr) = @_;
print "Wake on LAN packet send for '$mac_addr'\n";
} ],
print "Wake on LAN packet send for '$mac_addr'\n";
},
],
};

View file

@ -27,18 +27,18 @@ my $optmatch;
do {
$optmatch = 0;
if ($ARGV[0]) {
if ($ARGV[0] eq '--noproxy') {
shift @ARGV;
$disable_proxy = 1;
$optmatch = 1;
} elsif ($ARGV[0] eq '--nooutput') {
# we use this when starting task in CLI (suppress printing upid)
# for example 'pvesh --nooutput create /nodes/localhost/stopall'
shift @ARGV;
$opt_nooutput = 1;
$optmatch = 1;
}
}
if ($ARGV[0] eq '--noproxy') {
shift @ARGV;
$disable_proxy = 1;
$optmatch = 1;
} elsif ($ARGV[0] eq '--nooutput') {
# we use this when starting task in CLI (suppress printing upid)
# for example 'pvesh --nooutput create /nodes/localhost/stopall'
shift @ARGV;
$opt_nooutput = 1;
$optmatch = 1;
}
}
} while ($optmatch);
sub setup_environment {
@ -46,7 +46,7 @@ sub setup_environment {
}
sub complete_api_path {
my($text) = @_;
my ($text) = @_;
my ($dir, undef, $rest) = $text =~ m|^(.*/)?(([^/]*))?$|;
@ -60,16 +60,16 @@ sub complete_api_path {
my $di = dir_info($path);
if (my $children = $di->{children}) {
foreach my $c (@$children) {
if ($c =~ /^\Q$rest/) {
my $new = $dir ? "$dir$c" : $c;
push @$res, $new;
}
}
foreach my $c (@$children) {
if ($c =~ /^\Q$rest/) {
my $new = $dir ? "$dir$c" : $c;
push @$res, $new;
}
}
}
if (scalar(@$res) == 1) {
return [$res->[0], "$res->[0]/"];
return [$res->[0], "$res->[0]/"];
}
return $res;
@ -90,14 +90,18 @@ sub check_proxyto {
my $all_params = { %$uri_param, %$params };
if ($info->{proxyto} || $info->{proxyto_callback}) {
my $node = PVE::API2Tools::resolve_proxyto(
$rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $all_params);
my $node = PVE::API2Tools::resolve_proxyto(
$rpcenv,
$info->{proxyto_callback},
$info->{proxyto},
$all_params,
);
if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
die "proxy loop detected - aborting\n" if $disable_proxy;
my $remip = PVE::Cluster::remote_node_ip($node);
return ($node, $remip);
}
if ($node ne 'localhost' && ($node ne PVE::INotify::nodename())) {
die "proxy loop detected - aborting\n" if $disable_proxy;
my $remip = PVE::Cluster::remote_node_ip($node);
return ($node, $remip);
}
}
return undef;
@ -108,32 +112,32 @@ sub proxy_handler {
my $args = [];
foreach my $key (keys %$param) {
next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
if (ref($param->{$key}) eq 'ARRAY') {
push @$args, "--$key", $_ for $param->{$key}->@*;
} else {
push @$args, "--$key", $_ for split(/\0/, $param->{$key});
}
next if $key eq 'quiet' || $key eq 'output-format'; # just to be sure
if (ref($param->{$key}) eq 'ARRAY') {
push @$args, "--$key", $_ for $param->{$key}->@*;
} else {
push @$args, "--$key", $_ for split(/\0/, $param->{$key});
}
}
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)) {
my $cmdargs = [ String::ShellQuote::shell_quote(@$args) ];
push @pvesh_cmd, @$cmdargs;
my $cmdargs = [String::ShellQuote::shell_quote(@$args)];
push @pvesh_cmd, @$cmdargs;
}
my $res = '';
PVE::Tools::run_command(
[ $ssh_tunnel_cmd->@*, '--', @pvesh_cmd ],
errmsg => "proxy handler failed",
outfunc => sub { $res .= shift },
[$ssh_tunnel_cmd->@*, '--', @pvesh_cmd],
errmsg => "proxy handler failed",
outfunc => sub { $res .= shift },
);
my $decoded_json = eval { decode_json($res) };
if ($@) {
return $res; # do not error, '' (null) is valid too
return $res; # do not error, '' (null) is valid too
}
return $decoded_json;
}
@ -147,13 +151,13 @@ sub extract_children {
my $href = $lnk->{href};
if ($href =~ m/^\{(\S+)\}$/) {
my $prop = $1;
my $prop = $1;
foreach my $elem (sort {$a->{$prop} cmp $b->{$prop}} @$data) {
next if !ref($elem);
my $value = $elem->{$prop};
push @$res, $value;
}
foreach my $elem (sort { $a->{$prop} cmp $b->{$prop} } @$data) {
next if !ref($elem);
my $value = $elem->{$prop};
push @$res, $value;
}
}
return $res;
@ -166,11 +170,11 @@ sub dir_info {
my $uri_param = {};
my ($handler, $info, $pm) = PVE::API2->find_handler('GET', $path, $uri_param);
if ($handler && $info) {
eval {
my $data = $handler->handle($info, $uri_param);
my $lnk = PVE::JSONSchema::method_get_child_link($info);
$res->{children} = extract_children($lnk, $data);
}; # ignore errors ?
eval {
my $data = $handler->handle($info, $uri_param);
my $lnk = PVE::JSONSchema::method_get_child_link($info);
$res->{children} = extract_children($lnk, $data);
}; # ignore errors ?
}
return $res;
}
@ -182,34 +186,34 @@ sub resource_cap {
my ($handler, $info) = PVE::API2->find_handler('GET', $path);
if (!($handler && $info)) {
$res .= '--';
$res .= '--';
} else {
if (PVE::JSONSchema::method_get_child_link($info)) {
$res .= 'Dr';
} else {
$res .= '-r';
}
if (PVE::JSONSchema::method_get_child_link($info)) {
$res .= 'Dr';
} else {
$res .= '-r';
}
}
($handler, $info) = PVE::API2->find_handler('PUT', $path);
if (!($handler && $info)) {
$res .= '-';
$res .= '-';
} else {
$res .= 'w';
$res .= 'w';
}
($handler, $info) = PVE::API2->find_handler('POST', $path);
if (!($handler && $info)) {
$res .= '-';
$res .= '-';
} else {
$res .= 'c';
$res .= 'c';
}
($handler, $info) = PVE::API2->find_handler('DELETE', $path);
if (!($handler && $info)) {
$res .= '-';
$res .= '-';
} else {
$res .= 'd';
$res .= 'd';
}
return $res;
@ -224,52 +228,51 @@ sub extract_path_info {
my ($handler, $info);
my $test_path_properties = sub {
my ($method, $path) = @_;
($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
my ($method, $path) = @_;
($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
};
if (defined(my $cmd = $ARGV[0])) {
if (my $method = $method_map->{$cmd}) {
if (my $path = $ARGV[1]) {
$test_path_properties->($method, $path);
if (!defined($handler)) {
print STDERR "No '$cmd' handler defined for '$path'\n";
exit(1);
}
}
} elsif ($cmd eq 'bashcomplete') {
my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
my $args = PVE::Tools::split_args($cmdline);
if (defined(my $cmd = $args->[1])) {
if (my $method = $method_map->{$cmd}) {
if (my $path = $args->[2]) {
$test_path_properties->($method, $path);
}
}
}
}
if (my $method = $method_map->{$cmd}) {
if (my $path = $ARGV[1]) {
$test_path_properties->($method, $path);
if (!defined($handler)) {
print STDERR "No '$cmd' handler defined for '$path'\n";
exit(1);
}
}
} elsif ($cmd eq 'bashcomplete') {
my $cmdline = substr($ENV{COMP_LINE}, 0, $ENV{COMP_POINT});
my $args = PVE::Tools::split_args($cmdline);
if (defined(my $cmd = $args->[1])) {
if (my $method = $method_map->{$cmd}) {
if (my $path = $args->[2]) {
$test_path_properties->($method, $path);
}
}
}
}
}
return $info;
}
my $path_properties = {};
my $api_path_property = {
description => "API path.",
type => 'string',
completion => sub {
my ($cmd, $pname, $cur, $args) = @_;
return complete_api_path($cur);
my ($cmd, $pname, $cur, $args) = @_;
return complete_api_path($cur);
},
};
my $uri_param = {};
if (my $info = extract_path_info($uri_param)) {
foreach my $key (keys %{$info->{parameters}->{properties}}) {
next if defined($uri_param->{$key});
$path_properties->{$key} = $info->{parameters}->{properties}->{$key};
foreach my $key (keys %{ $info->{parameters}->{properties} }) {
next if defined($uri_param->{$key});
$path_properties->{$key} = $info->{parameters}->{properties}->{$key};
}
}
@ -285,7 +288,8 @@ my $extract_std_options = 1;
my $cond_add_standard_output_properties = sub {
my ($props) = @_;
my $keys = [ grep { !defined($props->{$_}) } keys %$PVE::RESTHandler::standard_output_options ];
my $keys =
[grep { !defined($props->{$_}) } keys %$PVE::RESTHandler::standard_output_options];
return PVE::RESTHandler::add_standard_output_properties($props, $keys);
};
@ -293,28 +297,30 @@ my $cond_add_standard_output_properties = sub {
my $handle_streamed_response = sub {
my ($download) = @_;
my ($fh, $path, $encoding, $type) =
$download->@{'fh', 'path', 'content-encoding', 'content-type'};
$download->@{ 'fh', 'path', 'content-encoding', 'content-type' };
die "{download} returned but neither fh nor path given\n" if !defined($fh) && !defined($path);
die "{download} returned but neither fh nor path given\n"
if !defined($fh) && !defined($path);
die "unknown 'content-encoding' $encoding\n" if defined($encoding) && $encoding ne 'gzip';
die "unknown 'content-type' $type\n" if defined($type) && $type !~ qw!^(?:text/plain|application/json)$!;
die "unknown 'content-type' $type\n"
if defined($type) && $type !~ qw!^(?:text/plain|application/json)$!;
if (defined($path)) {
open($fh, '<', $path) or die "open stream path '$path' for reading failed - $!\n";
open($fh, '<', $path) or die "open stream path '$path' for reading failed - $!\n";
}
local $/;
my $data = <$fh>;
if (defined($encoding)) {
my $out;
gunzip(\$data => \$out);
$data = $out;
my $out;
gunzip(\$data => \$out);
$data = $out;
}
if (defined($type) && $type eq 'application/json') {
$data = decode_json($data)->{data};
$data = decode_json($data)->{data};
}
return $data;
@ -328,7 +334,8 @@ sub call_api_method {
my $path = PVE::Tools::extract_param($param, 'api_path');
die "missing API path\n" if !defined($path);
my $stdopts = $extract_std_options
my $stdopts =
$extract_std_options
? PVE::RESTHandler::extract_standard_output_properties($param)
: {};
@ -337,28 +344,28 @@ sub call_api_method {
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
if (!$handler || !$info) {
die "no '$cmd' handler for '$path'\n";
die "no '$cmd' handler for '$path'\n";
}
my $data;
my ($node, $remip) = check_proxyto($info, $uri_param, $param);
if ($node) {
$data = proxy_handler($node, $remip, $path, $cmd, $param);
$data = proxy_handler($node, $remip, $path, $cmd, $param);
} else {
foreach my $p (keys %$uri_param) {
$param->{$p} = $uri_param->{$p};
}
foreach my $p (keys %$uri_param) {
$param->{$p} = $uri_param->{$p};
}
$data = $handler->handle($info, $param);
$data = $handler->handle($info, $param);
# TODO: remove 'download' check with PVE 9.0
if (
ref($data) eq 'HASH'
&& ref($data->{download}) eq 'HASH'
&& ($info->{download_allowed} || $info->{download})
) {
$data = $handle_streamed_response->($data->{download})
}
# TODO: remove 'download' check with PVE 9.0
if (
ref($data) eq 'HASH'
&& ref($data->{download}) eq 'HASH'
&& ($info->{download_allowed} || $info->{download})
) {
$data = $handle_streamed_response->($data->{download});
}
}
return if $opt_nooutput || $stdopts->{quiet};
@ -366,208 +373,226 @@ sub call_api_method {
PVE::CLIFormatter::print_api_result($data, $info->{returns}, undef, $stdopts);
}
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'ls',
path => 'ls',
method => 'GET',
description => "List child objects on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $path = PVE::Tools::extract_param($param, 'api_path');
my $path = PVE::Tools::extract_param($param, 'api_path');
my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
my $stdopts = PVE::RESTHandler::extract_standard_output_properties($param);
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
if (!$handler || !$info) {
die "no such resource '$path'\n";
}
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler('GET', $path, $uri_param);
if (!$handler || !$info) {
die "no such resource '$path'\n";
}
my $link = PVE::JSONSchema::method_get_child_link($info);
die "resource '$path' does not define child links\n" if !$link;
my $link = PVE::JSONSchema::method_get_child_link($info);
die "resource '$path' does not define child links\n" if !$link;
my $res;
my $res;
my ($node, $remip) = check_proxyto($info, $uri_param, $param);
if ($node) {
$res = proxy_handler($node, $remip, $path, 'ls', $param);
} else {
foreach my $p (keys %$uri_param) {
$param->{$p} = $uri_param->{$p};
}
my ($node, $remip) = check_proxyto($info, $uri_param, $param);
if ($node) {
$res = proxy_handler($node, $remip, $path, 'ls', $param);
} else {
foreach my $p (keys %$uri_param) {
$param->{$p} = $uri_param->{$p};
}
my $data = $handler->handle($info, $param);
my $data = $handler->handle($info, $param);
my $children = extract_children($link, $data);
my $children = extract_children($link, $data);
$res = [];
foreach my $c (@$children) {
my $item = { name => $c, capabilities => resource_cap("$path/$c")};
push @$res, $item;
}
}
$res = [];
foreach my $c (@$children) {
my $item = { name => $c, capabilities => resource_cap("$path/$c") };
push @$res, $item;
}
}
my $schema = { type => 'array', items => { type => 'object' }};
$stdopts->{sort_key} = 'name';
$stdopts->{noborder} //= 1;
$stdopts->{noheader} //= 1;
PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
my $schema = { type => 'array', items => { type => 'object' } };
$stdopts->{sort_key} = 'name';
$stdopts->{noborder} //= 1;
$stdopts->{noheader} //= 1;
PVE::CLIFormatter::print_api_result($res, $schema, ['capabilities', 'name'], $stdopts);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'get',
path => 'get',
method => 'GET',
description => "Call API GET on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
call_api_method('get', $param);
call_api_method('get', $param);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'set',
path => 'set',
method => 'PUT',
description => "Call API PUT on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
call_api_method('set', $param);
call_api_method('set', $param);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'create',
path => 'create',
method => 'POST',
description => "Call API POST on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
call_api_method('create', $param);
call_api_method('create', $param);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'delete',
path => 'delete',
method => 'DELETE',
description => "Call API DELETE on <api_path>.",
parameters => {
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
additionalProperties => 0,
properties => $cond_add_standard_output_properties->($path_properties),
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
call_api_method('delete', $param);
call_api_method('delete', $param);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'usage',
path => 'usage',
method => 'GET',
description => "print API usage information for <api_path>.",
parameters => {
additionalProperties => 0,
properties => {
api_path => $api_path_property,
verbose => {
description => "Verbose output format.",
type => 'boolean',
optional => 1,
},
returns => {
description => "Including schema for returned data.",
type => 'boolean',
optional => 1,
},
command => {
description => "API command.",
type => 'string',
enum => [ keys %$method_map ],
optional => 1,
},
},
additionalProperties => 0,
properties => {
api_path => $api_path_property,
verbose => {
description => "Verbose output format.",
type => 'boolean',
optional => 1,
},
returns => {
description => "Including schema for returned data.",
type => 'boolean',
optional => 1,
},
command => {
description => "API command.",
type => 'string',
enum => [keys %$method_map],
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $path = $param->{api_path};
my $path = $param->{api_path};
my $found = 0;
foreach my $cmd (qw(get set create delete)) {
next if $param->{command} && $cmd ne $param->{command};
my $method = $method_map->{$cmd};
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
next if !$handler;
$found = 1;
my $found = 0;
foreach my $cmd (qw(get set create delete)) {
next if $param->{command} && $cmd ne $param->{command};
my $method = $method_map->{$cmd};
my $uri_param = {};
my ($handler, $info) = PVE::API2->find_handler($method, $path, $uri_param);
next if !$handler;
$found = 1;
if ($param->{verbose}) {
print $handler->usage_str(
$info->{name}, "pvesh $cmd $path", undef, $uri_param, 'full');
if ($param->{verbose}) {
print $handler->usage_str(
$info->{name},
"pvesh $cmd $path",
undef,
$uri_param,
'full',
);
} else {
print "USAGE: " . $handler->usage_str(
$info->{name}, "pvesh $cmd $path", undef, $uri_param, 'short');
}
if ($param-> {returns}) {
my $schema = to_json($info->{returns}, {utf8 => 1, canonical => 1, pretty => 1 });
print "RETURNS: $schema\n";
}
}
} else {
print "USAGE: "
. $handler->usage_str(
$info->{name},
"pvesh $cmd $path",
undef,
$uri_param,
'short',
);
}
if ($param->{returns}) {
my $schema =
to_json($info->{returns}, { utf8 => 1, canonical => 1, pretty => 1 });
print "RETURNS: $schema\n";
}
}
if (!$found) {
if ($param->{command}) {
die "no '$param->{command}' handler for '$path'\n";
} else {
die "no such resource '$path'\n"
}
}
if (!$found) {
if ($param->{command}) {
die "no '$param->{command}' handler for '$path'\n";
} else {
die "no such resource '$path'\n";
}
}
return undef;
}});
return undef;
},
});
our $cmddef = {
usage => [ __PACKAGE__, 'usage', ['api_path']],
get => [ __PACKAGE__, 'get', ['api_path']],
ls => [ __PACKAGE__, 'ls', ['api_path']],
set => [ __PACKAGE__, 'set', ['api_path']],
create => [ __PACKAGE__, 'create', ['api_path']],
delete => [ __PACKAGE__, 'delete', ['api_path']],
usage => [__PACKAGE__, 'usage', ['api_path']],
get => [__PACKAGE__, 'get', ['api_path']],
ls => [__PACKAGE__, 'ls', ['api_path']],
set => [__PACKAGE__, 'set', ['api_path']],
create => [__PACKAGE__, 'create', ['api_path']],
delete => [__PACKAGE__, 'delete', ['api_path']],
};
1;

View file

@ -36,306 +36,329 @@ my $check_wanted_volid = sub {
my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid, $local_node);
die "storage '$storeid' is not replicatable\n"
if !$replicatable_storage_types->{$scfg->{type}};
if !$replicatable_storage_types->{ $scfg->{type} };
my ($vtype, undef, $ownervm) = PVE::Storage::parse_volname($storecfg, $volid);
die "volume '$volid' has wrong vtype ($vtype != 'images')\n"
if $vtype ne 'images';
if $vtype ne 'images';
die "volume '$volid' has wrong owner\n"
if !$ownervm || $vmid != $ownervm;
if !$ownervm || $vmid != $ownervm;
return $storeid;
};
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'prepare_local_job',
path => 'prepare_local_job',
method => 'POST',
description => "Prepare for starting a replication job. This is called on the target node before replication starts. This call is for internal use, and return a JSON object on stdout. The method first test if VM <vmid> reside on the local node. If so, stop immediately. After that the method scans all volume IDs for snapshots, and removes all replications snapshots with timestamps different than <last_sync>. It also removes any unused volumes. Returns a hash with boolean markers for all volumes with existing replication snapshots.",
description =>
"Prepare for starting a replication job. This is called on the target node before replication starts. This call is for internal use, and return a JSON object on stdout. The method first test if VM <vmid> reside on the local node. If so, stop immediately. After that the method scans all volume IDs for snapshots, and removes all replications snapshots with timestamps different than <last_sync>. It also removes any unused volumes. Returns a hash with boolean markers for all volumes with existing replication snapshots.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
'extra-args' => get_standard_option('extra-args', {
description => "The list of volume IDs to consider." }),
scan => {
description => "List of storage IDs to scan for stale volumes.",
type => 'string', format => 'pve-storage-id-list',
optional => 1,
},
force => {
description => "Allow to remove all existion volumes (empty volume list).",
type => 'boolean',
optional => 1,
default => 0,
},
last_sync => {
description => "Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots get removed.",
type => 'integer',
minimum => 0,
optional => 1,
},
parent_snapname => get_standard_option('pve-snapshot-name', {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
'extra-args' => get_standard_option(
'extra-args',
{ description => "The list of volume IDs to consider." },
),
scan => {
description => "List of storage IDs to scan for stale volumes.",
type => 'string',
format => 'pve-storage-id-list',
optional => 1,
},
force => {
description => "Allow to remove all existion volumes (empty volume list).",
type => 'boolean',
optional => 1,
default => 0,
},
last_sync => {
description =>
"Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots get removed.",
type => 'integer',
minimum => 0,
optional => 1,
},
parent_snapname => get_standard_option('pve-snapshot-name', {
optional => 1,
}),
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $logfunc = sub {
my ($msg) = @_;
print STDERR "$msg\n";
};
my $logfunc = sub {
my ($msg) = @_;
print STDERR "$msg\n";
};
my $local_node = PVE::INotify::nodename();
my $local_node = PVE::INotify::nodename();
die "no volumes specified\n"
if !$param->{force} && !scalar(@{$param->{'extra-args'}});
die "no volumes specified\n"
if !$param->{force} && !scalar(@{ $param->{'extra-args'} });
my ($vmid, undef, $jobid) = PVE::ReplicationConfig::parse_replication_job_id($param->{id});
my ($vmid, undef, $jobid) =
PVE::ReplicationConfig::parse_replication_job_id($param->{id});
my $vms = PVE::Cluster::get_vmlist();
die "guest '$vmid' is on local node\n"
if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node;
my $vms = PVE::Cluster::get_vmlist();
die "guest '$vmid' is on local node\n"
if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node;
my $last_sync = $param->{last_sync} // 0;
my $parent_snapname = $param->{parent_snapname};
my $last_sync = $param->{last_sync} // 0;
my $parent_snapname = $param->{parent_snapname};
my $storecfg = PVE::Storage::config();
my $storecfg = PVE::Storage::config();
# compute list of storages we want to scan
my $storage_hash = {};
foreach my $storeid (PVE::Tools::split_list($param->{scan})) {
my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid, $local_node, 1);
next if !$scfg; # simply ignore unavailable storages here
die "storage '$storeid' is not replicatable\n" if !$replicatable_storage_types->{$scfg->{type}};
$storage_hash->{$storeid} = 1;
}
# compute list of storages we want to scan
my $storage_hash = {};
foreach my $storeid (PVE::Tools::split_list($param->{scan})) {
my $scfg = PVE::Storage::storage_check_enabled($storecfg, $storeid, $local_node, 1);
next if !$scfg; # simply ignore unavailable storages here
die "storage '$storeid' is not replicatable\n"
if !$replicatable_storage_types->{ $scfg->{type} };
$storage_hash->{$storeid} = 1;
}
my $wanted_volids = {};
foreach my $volid (@{$param->{'extra-args'}}) {
my $storeid = $check_wanted_volid->($storecfg, $vmid, $volid, $local_node);
$wanted_volids->{$volid} = 1;
$storage_hash->{$storeid} = 1;
}
my $storage_list = [ sort keys %$storage_hash ];
my $wanted_volids = {};
foreach my $volid (@{ $param->{'extra-args'} }) {
my $storeid = $check_wanted_volid->($storecfg, $vmid, $volid, $local_node);
$wanted_volids->{$volid} = 1;
$storage_hash->{$storeid} = 1;
}
my $storage_list = [sort keys %$storage_hash];
# activate all used storage
my $cache = {};
PVE::Storage::activate_storage_list($storecfg, $storage_list, $cache);
# activate all used storage
my $cache = {};
PVE::Storage::activate_storage_list($storecfg, $storage_list, $cache);
my $snapname = PVE::ReplicationState::replication_snapshot_name($jobid, $last_sync);
my $snapname = PVE::ReplicationState::replication_snapshot_name($jobid, $last_sync);
# find replication snapshots
my $volids = [];
foreach my $storeid (@$storage_list) {
my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
my $images = $plugin->list_images($storeid, $scfg, $vmid, undef, $cache);
push @$volids, map { $_->{volid} } @$images;
}
my ($local_snapshots, $cleaned_replicated_volumes) = PVE::Replication::prepare($storecfg, $volids, $jobid, $last_sync, $parent_snapname, $logfunc);
for my $volid ($volids->@*) {
next if $wanted_volids->{$volid};
# find replication snapshots
my $volids = [];
foreach my $storeid (@$storage_list) {
my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
my $plugin = PVE::Storage::Plugin->lookup($scfg->{type});
my $images = $plugin->list_images($storeid, $scfg, $vmid, undef, $cache);
push @$volids, map { $_->{volid} } @$images;
}
my ($local_snapshots, $cleaned_replicated_volumes) = PVE::Replication::prepare(
$storecfg, $volids, $jobid, $last_sync, $parent_snapname, $logfunc,
);
for my $volid ($volids->@*) {
next if $wanted_volids->{$volid};
my $stale = $cleaned_replicated_volumes->{$volid};
# prepare() will not remove the last_sync snapshot, but if the volume was used by the
# job and is not wanted anymore, it is stale too. And not removing it now might cause
# it to be missed later, because the relevant storage might not get scanned anymore.
$stale ||= grep {
PVE::Replication::is_replication_snapshot($_, $jobid)
} keys %{$local_snapshots->{$volid} // {}};
my $stale = $cleaned_replicated_volumes->{$volid};
# prepare() will not remove the last_sync snapshot, but if the volume was used by the
# job and is not wanted anymore, it is stale too. And not removing it now might cause
# it to be missed later, because the relevant storage might not get scanned anymore.
$stale ||= grep {
PVE::Replication::is_replication_snapshot($_, $jobid)
} keys %{ $local_snapshots->{$volid} // {} };
if ($stale) {
$logfunc->("$jobid: delete stale volume '$volid'");
PVE::Storage::vdisk_free($storecfg, $volid);
delete $local_snapshots->{$volid};
}
}
if ($stale) {
$logfunc->("$jobid: delete stale volume '$volid'");
PVE::Storage::vdisk_free($storecfg, $volid);
delete $local_snapshots->{$volid};
}
}
print to_json($local_snapshots) . "\n";
print to_json($local_snapshots) . "\n";
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'finalize_local_job',
path => 'finalize_local_job',
method => 'POST',
description => "Finalize a replication job. This removes all replications snapshots with timestamps different than <last_sync>.",
description =>
"Finalize a replication job. This removes all replications snapshots with timestamps different than <last_sync>.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
'extra-args' => get_standard_option('extra-args', {
description => "The list of volume IDs to consider." }),
last_sync => {
description => "Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots gets removed.",
type => 'integer',
minimum => 0,
optional => 1,
},
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
'extra-args' => get_standard_option(
'extra-args',
{ description => "The list of volume IDs to consider." },
),
last_sync => {
description =>
"Time (UNIX epoch) of last successful sync. If not specified, all replication snapshots gets removed.",
type => 'integer',
minimum => 0,
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my ($vmid, undef, $jobid) = PVE::ReplicationConfig::parse_replication_job_id($param->{id});
my $last_sync = $param->{last_sync} // 0;
my ($vmid, undef, $jobid) =
PVE::ReplicationConfig::parse_replication_job_id($param->{id});
my $last_sync = $param->{last_sync} // 0;
my $local_node = PVE::INotify::nodename();
my $local_node = PVE::INotify::nodename();
my $vms = PVE::Cluster::get_vmlist();
die "guest '$vmid' is on local node\n"
if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node;
my $vms = PVE::Cluster::get_vmlist();
die "guest '$vmid' is on local node\n"
if $vms->{ids}->{$vmid} && $vms->{ids}->{$vmid}->{node} eq $local_node;
my $storecfg = PVE::Storage::config();
my $storecfg = PVE::Storage::config();
my $volids = [];
my $volids = [];
die "no volumes specified\n" if !scalar(@{$param->{'extra-args'}});
die "no volumes specified\n" if !scalar(@{ $param->{'extra-args'} });
foreach my $volid (@{$param->{'extra-args'}}) {
$check_wanted_volid->($storecfg, $vmid, $volid, $local_node);
push @$volids, $volid;
}
foreach my $volid (@{ $param->{'extra-args'} }) {
$check_wanted_volid->($storecfg, $vmid, $volid, $local_node);
push @$volids, $volid;
}
$volids = [ sort @$volids ];
$volids = [sort @$volids];
my $logfunc = sub {
my ($msg) = @_;
print STDERR "$msg\n";
};
my $logfunc = sub {
my ($msg) = @_;
print STDERR "$msg\n";
};
PVE::Replication::prepare($storecfg, $volids, $jobid, $last_sync, undef, $logfunc);
PVE::Replication::prepare($storecfg, $volids, $jobid, $last_sync, undef, $logfunc);
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'run',
path => 'run',
method => 'POST',
description => "This method is called by the systemd-timer and executes all (or a specific) sync jobs.",
description =>
"This method is called by the systemd-timer and executes all (or a specific) sync jobs.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id', { optional => 1 }),
verbose => {
description => "Print more verbose logs to stdout.",
type => 'boolean',
default => 0,
optional => 1,
},
mail => {
description => "Send an email notification in case of a failure.",
type => 'boolean',
default => 0,
optional => 1,
},
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id', { optional => 1 }),
verbose => {
description => "Print more verbose logs to stdout.",
type => 'boolean',
default => 0,
optional => 1,
},
mail => {
description => "Send an email notification in case of a failure.",
type => 'boolean',
default => 0,
optional => 1,
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
die "Mail and id are mutually exclusive!\n"
if $param->{id} && $param->{mail};
die "Mail and id are mutually exclusive!\n"
if $param->{id} && $param->{mail};
my $logfunc;
my $logfunc;
if ($param->{verbose}) {
$logfunc = sub {
my ($msg) = @_;
print "$msg\n";
};
}
if ($param->{verbose}) {
$logfunc = sub {
my ($msg) = @_;
print "$msg\n";
};
}
if (my $id = extract_param($param, 'id')) {
if (my $id = extract_param($param, 'id')) {
PVE::API2::Replication::run_single_job($id, undef, $logfunc);
PVE::API2::Replication::run_single_job($id, undef, $logfunc);
} else {
} else {
PVE::API2::Replication::run_jobs(undef, $logfunc, 0, $param->{mail});
}
PVE::API2::Replication::run_jobs(undef, $logfunc, 0, $param->{mail});
}
return undef;
}});
return undef;
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'enable',
path => 'enable',
method => 'POST',
description => "Enable a replication job.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
$param->{disable} = 0;
$param->{disable} = 0;
return PVE::API2::ReplicationConfig->update($param);
}});
return PVE::API2::ReplicationConfig->update($param);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'disable',
path => 'disable',
method => 'POST',
description => "Disable a replication job.",
parameters => {
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
additionalProperties => 0,
properties => {
id => get_standard_option('pve-replication-id'),
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
$param->{disable} = 1;
$param->{disable} = 1;
return PVE::API2::ReplicationConfig->update($param);
}});
return PVE::API2::ReplicationConfig->update($param);
},
});
__PACKAGE__->register_method ({
__PACKAGE__->register_method({
name => 'set_state',
path => '',
protected => 1,
method => 'POST',
description => "Set the job replication state on migration. This call is for internal use. It will accept the job state as ja JSON obj.",
description =>
"Set the job replication state on migration. This call is for internal use. It will accept the job state as ja JSON obj.",
permissions => {
check => ['perm', '/storage', ['Datastore.Allocate']],
check => ['perm', '/storage', ['Datastore.Allocate']],
},
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_vmid }),
state => {
description => "Job state as JSON decoded string.",
type => 'string',
},
},
additionalProperties => 0,
properties => {
vmid =>
get_standard_option('pve-vmid', { completion => \&PVE::Cluster::complete_vmid }),
state => {
description => "Job state as JSON decoded string.",
type => 'string',
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $vmid = extract_param($param, 'vmid');
my $json_string = extract_param($param, 'state');
my $remote_job_state= decode_json $json_string;
my $vmid = extract_param($param, 'vmid');
my $json_string = extract_param($param, 'state');
my $remote_job_state = decode_json $json_string;
PVE::ReplicationState::write_vmid_job_states($remote_job_state, $vmid);
return undef;
}});
PVE::ReplicationState::write_vmid_job_states($remote_job_state, $vmid);
return undef;
},
});
my $print_job_list = sub {
my ($list) = @_;
@ -345,14 +368,16 @@ my $print_job_list = sub {
printf($format, "JobID", "Target", "Schedule", "Rate", "Enabled");
foreach my $job (sort { $a->{guest} <=> $b->{guest} } @$list) {
my $plugin = PVE::ReplicationConfig->lookup($job->{type});
my $tid = $plugin->get_unique_target_id($job);
my $plugin = PVE::ReplicationConfig->lookup($job->{type});
my $tid = $plugin->get_unique_target_id($job);
printf($format, $job->{id}, $tid,
defined($job->{schedule}) ? $job->{schedule} : '*/15',
defined($job->{rate}) ? $job->{rate} : '-',
$job->{disable} ? 'no' : 'yes'
);
printf(
$format,
$job->{id}, $tid,
defined($job->{schedule}) ? $job->{schedule} : '*/15',
defined($job->{rate}) ? $job->{rate} : '-',
$job->{disable} ? 'no' : 'yes',
);
}
};
@ -361,56 +386,73 @@ my $print_job_status = sub {
my $format = "%-10s %-10s %-20s %20s %20s %10s %10s %s\n";
printf($format, "JobID", "Enabled", "Target", "LastSync", "NextSync", "Duration", "FailCount", "State");
printf($format,
"JobID",
"Enabled",
"Target",
"LastSync",
"NextSync",
"Duration",
"FailCount",
"State");
foreach my $job (sort { $a->{guest} <=> $b->{guest} } @$list) {
my $plugin = PVE::ReplicationConfig->lookup($job->{type});
my $tid = $plugin->get_unique_target_id($job);
my $plugin = PVE::ReplicationConfig->lookup($job->{type});
my $tid = $plugin->get_unique_target_id($job);
my $timestr = '-';
if ($job->{last_sync}) {
$timestr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{last_sync}));
}
my $timestr = '-';
if ($job->{last_sync}) {
$timestr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{last_sync}));
}
my $nextstr = '-';
if (my $next = $job->{next_sync}) {
my $now = time();
if ($next > $now) {
$nextstr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{next_sync}));
} else {
$nextstr = 'pending';
}
}
my $nextstr = '-';
if (my $next = $job->{next_sync}) {
my $now = time();
if ($next > $now) {
$nextstr = strftime("%Y-%m-%d_%H:%M:%S", localtime($job->{next_sync}));
} else {
$nextstr = 'pending';
}
}
my $state = $job->{pid} ? "SYNCING" : $job->{error} // 'OK';
my $enabled = $job->{disable} ? 'No' : 'Yes';
my $state = $job->{pid} ? "SYNCING" : $job->{error} // 'OK';
my $enabled = $job->{disable} ? 'No' : 'Yes';
printf($format, $job->{id}, $enabled, $tid,
$timestr, $nextstr, $job->{duration} // '-',
$job->{fail_count}, $state);
printf($format,
$job->{id}, $enabled, $tid, $timestr, $nextstr, $job->{duration} // '-',
$job->{fail_count}, $state);
}
};
our $cmddef = {
status => [ 'PVE::API2::Replication', 'status', [], { node => $nodename }, $print_job_status ],
'schedule-now' => [ 'PVE::API2::Replication', 'schedule_now', ['id'], { node => $nodename }],
status =>
['PVE::API2::Replication', 'status', [], { node => $nodename }, $print_job_status],
'schedule-now' => ['PVE::API2::Replication', 'schedule_now', ['id'], { node => $nodename }],
list => [ 'PVE::API2::ReplicationConfig', 'index' , [], {}, $print_job_list ],
read => [ 'PVE::API2::ReplicationConfig', 'read' , ['id'], {},
sub { my $res = shift; print to_json($res, { utf8 => 1, pretty => 1, canonical => 1}); }],
update => [ 'PVE::API2::ReplicationConfig', 'update' , ['id'], {} ],
delete => [ 'PVE::API2::ReplicationConfig', 'delete' , ['id'], {} ],
'create-local-job' => [ 'PVE::API2::ReplicationConfig', 'create' , ['id', 'target'],
{ type => 'local' } ],
list => ['PVE::API2::ReplicationConfig', 'index', [], {}, $print_job_list],
read => [
'PVE::API2::ReplicationConfig',
'read',
['id'],
{},
sub {
my $res = shift;
print to_json($res, { utf8 => 1, pretty => 1, canonical => 1 });
},
],
update => ['PVE::API2::ReplicationConfig', 'update', ['id'], {}],
delete => ['PVE::API2::ReplicationConfig', 'delete', ['id'], {}],
'create-local-job' =>
['PVE::API2::ReplicationConfig', 'create', ['id', 'target'], { type => 'local' }],
enable => [ __PACKAGE__, 'enable', ['id'], {}],
disable => [ __PACKAGE__, 'disable', ['id'], {}],
enable => [__PACKAGE__, 'enable', ['id'], {}],
disable => [__PACKAGE__, 'disable', ['id'], {}],
'prepare-local-job' => [ __PACKAGE__, 'prepare_local_job', ['id', 'extra-args'], {} ],
'finalize-local-job' => [ __PACKAGE__, 'finalize_local_job', ['id', 'extra-args'], {} ],
'prepare-local-job' => [__PACKAGE__, 'prepare_local_job', ['id', 'extra-args'], {}],
'finalize-local-job' => [__PACKAGE__, 'finalize_local_job', ['id', 'extra-args'], {}],
run => [ __PACKAGE__ , 'run'],
'set-state' => [ __PACKAGE__ , 'set_state', ['vmid', 'state']],
run => [__PACKAGE__, 'run'],
'set-state' => [__PACKAGE__, 'set_state', ['vmid', 'state']],
};
1;

View file

@ -25,42 +25,51 @@ __PACKAGE__->register_method({
name => 'set_offline_key',
path => 'set_offline_key',
method => 'POST',
description => "Internal use only! To set an offline key, use the package proxmox-offline-mirror-helper instead.",
description =>
"Internal use only! To set an offline key, use the package proxmox-offline-mirror-helper instead.",
parameters => {
additionalProperties => 0,
properties => {
data => {
description => "A signed subscription info blob",
type => "string",
},
},
additionalProperties => 0,
properties => {
data => {
description => "A signed subscription info blob",
type => "string",
},
},
},
returns => { type => 'null' },
code => sub {
my ($param) = @_;
my ($param) = @_;
my $info = decode_json(decode_base64($param->{data}));
my $info = decode_json(decode_base64($param->{data}));
$info = Proxmox::RS::Subscription::check_signature($info);
$info = Proxmox::RS::Subscription::check_server_id($info);
$info = Proxmox::RS::Subscription::check_age($info, 0);
$info = Proxmox::RS::Subscription::check_signature($info);
$info = Proxmox::RS::Subscription::check_server_id($info);
$info = Proxmox::RS::Subscription::check_age($info, 0);
PVE::API2::Subscription::check_key($info->{key}, PVE::API2::Subscription::get_sockets());
PVE::API2::Subscription::check_key($info->{key},
PVE::API2::Subscription::get_sockets());
PVE::API2::Subscription::write_etc_subscription($info);
PVE::API2::Subscription::write_etc_subscription($info);
return;
}});
return;
},
});
our $cmddef = {
update => [ 'PVE::API2::Subscription', 'update', undef, { node => $nodename } ],
get => [ 'PVE::API2::Subscription', 'get', undef, { node => $nodename }, sub {
my $info = shift;
print "$_: $info->{$_}\n" for sort keys %$info;
}],
set => [ 'PVE::API2::Subscription', 'set', ['key'], { node => $nodename } ],
"set-offline-key" => [ __PACKAGE__, 'set_offline_key', ['data'] ],
delete => [ 'PVE::API2::Subscription', 'delete', undef, { node => $nodename } ],
update => ['PVE::API2::Subscription', 'update', undef, { node => $nodename }],
get => [
'PVE::API2::Subscription',
'get',
undef,
{ node => $nodename },
sub {
my $info = shift;
print "$_: $info->{$_}\n" for sort keys %$info;
},
],
set => ['PVE::API2::Subscription', 'set', ['key'], { node => $nodename }],
"set-offline-key" => [__PACKAGE__, 'set_offline_key', ['data']],
delete => ['PVE::API2::Subscription', 'delete', undef, { node => $nodename }],
};
1;

View file

@ -14,12 +14,17 @@ sub setup_environment {
}
# Note: use string 'vmid' as $arg_param option, to allow vmid lists
our $cmddef = [ 'PVE::API2::VZDump', 'vzdump', 'vmid', undef,
sub {
my $upid = shift;
exit(0) if $upid eq 'OK';
my $status = PVE::Tools::upid_read_status($upid);
exit(PVE::Tools::upid_status_is_error($status) ? -1 : 0);
}];
our $cmddef = [
'PVE::API2::VZDump',
'vzdump',
'vmid',
undef,
sub {
my $upid = shift;
exit(0) if $upid eq 'OK';
my $status = PVE::Tools::upid_read_status($upid);
exit(PVE::Tools::upid_status_is_error($status) ? -1 : 0);
},
];
1;

View file

@ -5,14 +5,14 @@ use v5.36;
use PVE::pvecfg;
my sub get_current_pve_major_release {
my $release_tuples = [ split(/\./, PVE::pvecfg::release()) ];
my $release_tuples = [split(/\./, PVE::pvecfg::release())];
return $release_tuples->[0];
}
my $_ceph_release_info;
my sub get_ceph_release_def {
if (defined($_ceph_release_info)) {
return $_ceph_release_info;
return $_ceph_release_info;
}
# Track all ceph releases (from a initial cut-off) with some metainfo.
@ -34,56 +34,57 @@ my sub get_ceph_release_def {
# NOTE: very old releases got left out from the list, but there's _no_ need to clean-up
# periodically check https://docs.ceph.com/en/latest/releases/ for all past releases
my $ceph_release_info = {
octopus => {
release => '15.2',
'initial-upstream-release' => '2020-03-23',
'estimated-end-of-upstream-support' => '2022-08-09',
'available-for-pve-release' => {
6 => 1,
7 => 1,
},
},
pacific => {
release => '16.2',
'initial-upstream-release' => '2021-03-31',
'estimated-end-of-upstream-support' => '2024-03-04',
'available-for-pve-release' => {
7 => 1,
},
},
quincy => {
release => '17.2',
'current-backend-default' => 1,
'initial-upstream-release' => '2022-04-19',
'estimated-end-of-upstream-support' => '2024-06-01',
'available-for-pve-release' => {
7 => 1,
8 => 1,
},
},
reef => {
release => '18.2',
'initial-upstream-release' => '2023-08-07',
'estimated-end-of-upstream-support' => '2025-08-01',
'available-for-pve-release' => {
8 => 1,
},
},
squid => {
release => '19.2',
'initial-upstream-release' => '2024-09-27',
'estimated-end-of-upstream-support' => '2026-09-19',
'available-for-pve-release' => {
8 => 1,
},
},
octopus => {
release => '15.2',
'initial-upstream-release' => '2020-03-23',
'estimated-end-of-upstream-support' => '2022-08-09',
'available-for-pve-release' => {
6 => 1,
7 => 1,
},
},
pacific => {
release => '16.2',
'initial-upstream-release' => '2021-03-31',
'estimated-end-of-upstream-support' => '2024-03-04',
'available-for-pve-release' => {
7 => 1,
},
},
quincy => {
release => '17.2',
'current-backend-default' => 1,
'initial-upstream-release' => '2022-04-19',
'estimated-end-of-upstream-support' => '2024-06-01',
'available-for-pve-release' => {
7 => 1,
8 => 1,
},
},
reef => {
release => '18.2',
'initial-upstream-release' => '2023-08-07',
'estimated-end-of-upstream-support' => '2025-08-01',
'available-for-pve-release' => {
8 => 1,
},
},
squid => {
release => '19.2',
'initial-upstream-release' => '2024-09-27',
'estimated-end-of-upstream-support' => '2026-09-19',
'available-for-pve-release' => {
8 => 1,
},
},
};
my $current_pve_major_release = get_current_pve_major_release();
for my $codename (sort keys $ceph_release_info->%*) {
my $ceph_release = $ceph_release_info->{$codename};
$ceph_release->{codename} = $codename;
$ceph_release->{'available-for-current-pve-release'} = $ceph_release->{'available-for-pve-release'}->{$current_pve_major_release};
my $ceph_release = $ceph_release_info->{$codename};
$ceph_release->{codename} = $codename;
$ceph_release->{'available-for-current-pve-release'} =
$ceph_release->{'available-for-pve-release'}->{$current_pve_major_release};
}
$_ceph_release_info = $ceph_release_info;
@ -95,15 +96,16 @@ sub get_ceph_release_info($codename) {
}
my $_available_ceph_releases;
sub get_all_available_ceph_releases {
if (!defined($_available_ceph_releases)) {
my $ceph_releases = get_ceph_release_def();
$_available_ceph_releases = {};
for my $codename (sort keys $ceph_releases->%*) {
if ($ceph_releases->{$codename}->{'available-for-current-pve-release'}) {
$_available_ceph_releases->{$codename} = $ceph_releases->{$codename};
}
}
my $ceph_releases = get_ceph_release_def();
$_available_ceph_releases = {};
for my $codename (sort keys $ceph_releases->%*) {
if ($ceph_releases->{$codename}->{'available-for-current-pve-release'}) {
$_available_ceph_releases->{$codename} = $ceph_releases->{$codename};
}
}
}
return $_available_ceph_releases;
}
@ -112,21 +114,22 @@ sub get_available_ceph_release_codenames($include_unstable_releases = 0) {
my $available_releases = get_all_available_ceph_releases();
return $include_unstable_releases
? [ sort keys $available_releases->%* ]
: [ grep { !$available_releases->{$_}->{unsupported} } sort keys $available_releases->%* ]
;
? [sort keys $available_releases->%*]
: [grep { !$available_releases->{$_}->{unsupported} } sort keys $available_releases->%*];
}
my $_default_ceph_release_codename;
sub get_default_ceph_release_codename {
if (!defined($_default_ceph_release_codename)) {
my $ceph_releases = get_all_available_ceph_releases();
my @default_release = grep { $ceph_releases->{$_}->{'current-backend-default'} } keys $ceph_releases->%*;
die "internal error: got multiple ceph releases with 'current-backend-default' set\n"
if scalar(@default_release) > 1;
die "internal error: got no ceph releases with 'current-backend-default' set\n"
if scalar(@default_release) < 1;
$_default_ceph_release_codename = $default_release[0];
my $ceph_releases = get_all_available_ceph_releases();
my @default_release =
grep { $ceph_releases->{$_}->{'current-backend-default'} } keys $ceph_releases->%*;
die "internal error: got multiple ceph releases with 'current-backend-default' set\n"
if scalar(@default_release) > 1;
die "internal error: got no ceph releases with 'current-backend-default' set\n"
if scalar(@default_release) < 1;
$_default_ceph_release_codename = $default_release[0];
}
return $_default_ceph_release_codename;
}

View file

@ -19,21 +19,29 @@ sub get_local_services {
my $res = {};
for my $type (qw(mds mgr mon)) {
$res->{$type} = {};
$res->{$type} = {};
my $path = "/etc/systemd/system/ceph-$type.target.wants";
my $regex = "ceph-$type\@(.*)\.service";
PVE::Tools::dir_glob_foreach($path, $regex, sub {
my (undef, $id) = @_;
$res->{$type}->{$id}->{service} = 1;
});
my $path = "/etc/systemd/system/ceph-$type.target.wants";
my $regex = "ceph-$type\@(.*)\.service";
PVE::Tools::dir_glob_foreach(
$path,
$regex,
sub {
my (undef, $id) = @_;
$res->{$type}->{$id}->{service} = 1;
},
);
$path = "/var/lib/ceph/$type";
$regex = "([^-]+)-(.*)";
PVE::Tools::dir_glob_foreach($path, $regex, sub {
my (undef, $clustername, $id) = @_;
$res->{$type}->{$id}->{direxists} = 1;
});
$path = "/var/lib/ceph/$type";
$regex = "([^-]+)-(.*)";
PVE::Tools::dir_glob_foreach(
$path,
$regex,
sub {
my (undef, $clustername, $id) = @_;
$res->{$type}->{$id}->{direxists} = 1;
},
);
}
return $res;
}
@ -42,8 +50,8 @@ sub broadcast_ceph_services {
my $services = get_local_services();
for my $type (keys %$services) {
my $data = encode_json($services->{$type});
PVE::Cluster::broadcast_node_kv("ceph-$type", $data);
my $data = encode_json($services->{$type});
PVE::Cluster::broadcast_node_kv("ceph-$type", $data);
}
}
@ -55,19 +63,23 @@ sub broadcast_ceph_versions {
my $nodename = PVE::INotify::nodename();
my $old_versions = PVE::Cluster::get_node_kv("ceph-versions", $nodename);
if (length(my $old_version_raw = $old_versions->{$nodename})) {
my $old = eval { decode_json($old_version_raw) };
warn "failed to parse ceph-versions '$old_version_raw' as JSON - $@" if $@; # should not happen
if (defined($old) && $old->{buildcommit} eq $buildcommit && $old->{version}->{str} eq $version) {
return; # up to date, nothing to do so avoid (not exactly cheap) broadcast
}
my $old = eval { decode_json($old_version_raw) };
warn "failed to parse ceph-versions '$old_version_raw' as JSON - $@" if $@; # should not happen
if (
defined($old)
&& $old->{buildcommit} eq $buildcommit
&& $old->{version}->{str} eq $version
) {
return; # up to date, nothing to do so avoid (not exactly cheap) broadcast
}
}
my $node_versions = {
version => {
str => $version,
parts => $vers_parts,
},
buildcommit => $buildcommit,
version => {
str => $version,
parts => $vers_parts,
},
buildcommit => $buildcommit,
};
PVE::Cluster::broadcast_node_kv("ceph-versions", encode_json($node_versions));
}
@ -76,9 +88,11 @@ sub get_ceph_versions {
my $res;
if (defined(my $versions = PVE::Cluster::get_node_kv("ceph-versions"))) {
$res = {
map { eval { $_ => decode_json($versions->{$_}) } } keys %$versions
};
$res = {
map {
eval { $_ => decode_json($versions->{$_}) }
} keys %$versions
};
}
return $res;
@ -89,7 +103,9 @@ sub get_cluster_service {
my $raw = PVE::Cluster::get_node_kv("ceph-$type");
my $res = {
map { $_ => eval { decode_json($raw->{$_}) } } keys $raw->%*
map {
$_ => eval { decode_json($raw->{$_}) }
} keys $raw->%*
};
return $res;
@ -99,9 +115,9 @@ sub ceph_service_cmd {
my ($action, $service) = @_;
if ($service && $service =~ m/^(mon|osd|mds|mgr|radosgw)(\.(${\SERVICE_REGEX}))?$/) {
$service = defined($3) ? "ceph-$1\@$3" : "ceph-$1.target";
$service = defined($3) ? "ceph-$1\@$3" : "ceph-$1.target";
} else {
$service = "ceph.target";
$service = "ceph.target";
}
run_command(['/bin/systemctl', $action, $service]);
@ -114,46 +130,46 @@ sub get_services_info {
my $services = get_cluster_service($type);
foreach my $host (sort keys %$services) {
foreach my $id (sort keys %{$services->{$host}}) {
my $service = $result->{$id} = $services->{$host}->{$id};
$service->{host} = $host;
$service->{name} = $id;
$service->{state} = 'unknown';
if ($service->{service}) {
$service->{state} = 'stopped';
}
}
foreach my $id (sort keys %{ $services->{$host} }) {
my $service = $result->{$id} = $services->{$host}->{$id};
$service->{host} = $host;
$service->{name} = $id;
$service->{state} = 'unknown';
if ($service->{service}) {
$service->{state} = 'stopped';
}
}
}
if (!$cfg) {
$cfg = cfs_read_file('ceph.conf');
$cfg = cfs_read_file('ceph.conf');
}
foreach my $section (keys %$cfg) {
my $d = $cfg->{$section};
if ($section =~ m/^$type\.(\S+)$/) {
my $id = $1;
my $service = $result->{$id};
my $addr = $d->{"${type}_addr"} // $d->{public_addr} // $d->{host};
$service->{name} //= $id;
$service->{addr} //= $addr;
$service->{state} //= 'unknown';
$service->{host} //= $d->{host};
}
my $d = $cfg->{$section};
if ($section =~ m/^$type\.(\S+)$/) {
my $id = $1;
my $service = $result->{$id};
my $addr = $d->{"${type}_addr"} // $d->{public_addr} // $d->{host};
$service->{name} //= $id;
$service->{addr} //= $addr;
$service->{state} //= 'unknown';
$service->{host} //= $d->{host};
}
}
if (!$rados) {
return $result;
return $result;
}
my $metadata = $rados->mon_command({ prefix => "$type metadata" });
foreach my $info (@$metadata) {
my $id = $info->{name} // $info->{id};
my $service = $result->{$id};
$service->{ceph_version_short} = $info->{ceph_version_short};
$service->{ceph_version} = $info->{ceph_version};
$service->{host} //= $info->{hostname};
$service->{addr} //= $info->{addr};
my $id = $info->{name} // $info->{id};
my $service = $result->{$id};
$service->{ceph_version_short} = $info->{ceph_version_short};
$service->{ceph_version} = $info->{ceph_version};
$service->{host} //= $info->{hostname};
$service->{addr} //= $info->{addr};
}
return $result;
@ -166,10 +182,14 @@ sub list_local_mds_ids {
my $ceph_mds_data_dir = PVE::Ceph::Tools::get_config('ceph_mds_data_dir');
my $ccname = PVE::Ceph::Tools::get_config('ccname');
PVE::Tools::dir_glob_foreach($ceph_mds_data_dir, qr/$ccname-(\S+)/, sub {
my (undef, $mds_id) = @_;
push @$mds_list, $mds_id;
});
PVE::Tools::dir_glob_foreach(
$ceph_mds_data_dir,
qr/$ccname-(\S+)/,
sub {
my (undef, $mds_id) = @_;
push @$mds_list, $mds_id;
},
);
return $mds_list;
}
@ -180,38 +200,37 @@ sub get_cluster_mds_state {
my $mds_state = {};
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
my $add_state = sub {
my ($mds, $fsname) = @_;
my ($mds, $fsname) = @_;
my $state = {};
$state->{addr} = $mds->{addr};
$state->{rank} = $mds->{rank};
$state->{standby_replay} = $mds->{standby_replay} ? 1 : 0;
$state->{state} = $mds->{state};
$state->{fs_name} = $fsname;
my $state = {};
$state->{addr} = $mds->{addr};
$state->{rank} = $mds->{rank};
$state->{standby_replay} = $mds->{standby_replay} ? 1 : 0;
$state->{state} = $mds->{state};
$state->{fs_name} = $fsname;
$mds_state->{$mds->{name}} = $state;
$mds_state->{ $mds->{name} } = $state;
};
my $mds_dump = $rados->mon_command({ prefix => 'mds stat' });
my $fsmap = $mds_dump->{fsmap};
foreach my $mds (@{$fsmap->{standbys}}) {
$add_state->($mds);
foreach my $mds (@{ $fsmap->{standbys} }) {
$add_state->($mds);
}
for my $fs_info (@{$fsmap->{filesystems}}) {
my $active_mds = $fs_info->{mdsmap}->{info};
for my $fs_info (@{ $fsmap->{filesystems} }) {
my $active_mds = $fs_info->{mdsmap}->{info};
# normally there's only one active MDS, but we can have multiple active for
# different ranks (e.g., different cephs path hierarchy). So just add all.
foreach my $mds (values %$active_mds) {
$add_state->($mds, $fs_info->{mdsmap}->{fs_name});
}
# normally there's only one active MDS, but we can have multiple active for
# different ranks (e.g., different cephs path hierarchy). So just add all.
foreach my $mds (values %$active_mds) {
$add_state->($mds, $fs_info->{mdsmap}->{fs_name});
}
}
return $mds_state;
@ -221,22 +240,22 @@ sub is_mds_active {
my ($rados, $fs_name) = @_;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
my $mds_dump = $rados->mon_command({ prefix => 'mds stat' });
my $fsmap = $mds_dump->{fsmap}->{filesystems};
if (!($fsmap && scalar(@$fsmap) > 0)) {
return undef;
return undef;
}
for my $fs (@$fsmap) {
next if defined($fs_name) && $fs->{mdsmap}->{fs_name} ne $fs_name;
next if defined($fs_name) && $fs->{mdsmap}->{fs_name} ne $fs_name;
my $active_mds = $fs->{mdsmap}->{info};
for my $mds (values %$active_mds) {
return 1 if $mds->{state} eq 'up:active';
}
my $active_mds = $fs->{mdsmap}->{info};
for my $mds (values %$active_mds) {
return 1 if $mds->{state} eq 'up:active';
}
}
return 0;
@ -247,10 +266,10 @@ sub create_mds {
# `ceph fs status` fails with numeric only ID.
die "ID: $id, numeric only IDs are not supported\n"
if $id =~ /^\d+$/;
if $id =~ /^\d+$/;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
my $ccname = PVE::Ceph::Tools::get_config('ccname');
@ -259,7 +278,7 @@ sub create_mds {
my $service_name = "mds.$id";
die "ceph MDS directory '$service_dir' already exists\n"
if -d $service_dir;
if -d $service_dir;
print "creating MDS directory '$service_dir'\n";
eval { File::Path::mkpath($service_dir) };
@ -268,17 +287,17 @@ sub create_mds {
# http://docs.ceph.com/docs/luminous/install/manual-deployment/#adding-mds
my $priv = [
mon => 'allow profile mds',
osd => 'allow rwx',
mds => 'allow *',
mon => 'allow profile mds',
osd => 'allow rwx',
mds => 'allow *',
];
print "creating keys for '$service_name'\n";
my $output = $rados->mon_command({
prefix => 'auth get-or-create',
entity => $service_name,
caps => $priv,
format => 'plain',
prefix => 'auth get-or-create',
entity => $service_name,
caps => $priv,
format => 'plain',
});
PVE::Tools::file_set_contents($service_keyring, $output);
@ -294,13 +313,13 @@ sub create_mds {
broadcast_ceph_services();
return undef;
};
}
sub destroy_mds {
my ($id, $rados) = @_;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
my $ccname = PVE::Ceph::Tools::get_config('ccname');
@ -314,23 +333,23 @@ sub destroy_mds {
ceph_service_cmd('stop', $service_name);
if (-d $service_dir) {
print "removing ceph-mds directory '$service_dir'\n";
File::Path::remove_tree($service_dir);
print "removing ceph-mds directory '$service_dir'\n";
File::Path::remove_tree($service_dir);
} else {
warn "cannot cleanup MDS $id directory, '$service_dir' not found\n"
warn "cannot cleanup MDS $id directory, '$service_dir' not found\n";
}
print "removing ceph auth for '$service_name'\n";
$rados->mon_command({
prefix => 'auth del',
entity => $service_name,
format => 'plain'
});
prefix => 'auth del',
entity => $service_name,
format => 'plain',
});
broadcast_ceph_services();
return undef;
};
}
# MGR
@ -348,14 +367,14 @@ sub create_mgr {
mkdir $mgrdir;
print "creating keys for '$mgrname'\n";
my $output = $rados->mon_command({
prefix => 'auth get-or-create',
entity => $mgrname,
caps => [
mon => 'allow profile mgr',
osd => 'allow *',
mds => 'allow *',
],
format => 'plain'
prefix => 'auth get-or-create',
entity => $mgrname,
caps => [
mon => 'allow profile mgr',
osd => 'allow *',
mds => 'allow *',
],
format => 'plain',
});
PVE::Tools::file_set_contents($mgrkeyring, $output);
@ -380,7 +399,7 @@ sub destroy_mgr {
my $mgrdir = "/var/lib/ceph/mgr/$clustername-$mgrid";
die "ceph manager directory '$mgrdir' not found\n"
if ! -d $mgrdir;
if !-d $mgrdir;
print "disabling service 'ceph-mgr\@$mgrid.service'\n";
ceph_service_cmd('disable', $mgrname);
@ -392,7 +411,7 @@ sub destroy_mgr {
print "removing authkeys for $mgrname\n";
if (!$rados) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
$rados->mon_command({ prefix => 'auth del', entity => "$mgrname" });

View file

@ -61,9 +61,9 @@ sub get_local_version {
my $ceph_version;
run_command(
[ $ceph_service->{ceph_bin}, '--version' ],
noerr => $noerr,
outfunc => sub { $ceph_version = shift if !defined $ceph_version },
[$ceph_service->{ceph_bin}, '--version'],
noerr => $noerr,
outfunc => sub { $ceph_version = shift if !defined $ceph_version },
);
return undef if !defined $ceph_version;
@ -91,13 +91,13 @@ sub parse_ceph_version : prototype($) {
/x;
if ($ceph_version =~ /$re_ceph_version/) {
my ($version, $buildcommit) = ($1, $2);
my $subversions = [ split(/\.|-/, $version) ];
my ($version, $buildcommit) = ($1, $2);
my $subversions = [split(/\.|-/, $version)];
# return (version, buildid, [major, minor, ...]) : major;
return wantarray
? ($version, $buildcommit, $subversions)
: $subversions->[0];
# return (version, buildid, [major, minor, ...]) : major;
return wantarray
? ($version, $buildcommit, $subversions)
: $subversions->[0];
}
return undef;
@ -116,7 +116,7 @@ sub get_config {
my $value = $config_values->{$key} // $config_files->{$key};
die "no such ceph config '$key'" if ! defined($value);
die "no such ceph config '$key'" if !defined($value);
return $value;
}
@ -124,38 +124,41 @@ sub get_config {
sub purge_all_ceph_files {
my ($services) = @_;
my $is_local_mon;
my $monlist = [ split(',', PVE::CephConfig::get_monaddr_list($pve_ceph_cfgpath)) ];
my $monlist = [split(',', PVE::CephConfig::get_monaddr_list($pve_ceph_cfgpath))];
foreach my $service (keys %$services) {
my $type = $services->{$service};
next if (!%$type);
my $type = $services->{$service};
next if (!%$type);
foreach my $name (keys %$type) {
my $dir_exists = $type->{$name}->{direxists};
foreach my $name (keys %$type) {
my $dir_exists = $type->{$name}->{direxists};
$is_local_mon = grep($type->{$name}->{addr}, @$monlist)
if $service eq 'mon';
$is_local_mon = grep($type->{$name}->{addr}, @$monlist)
if $service eq 'mon';
my $path = "/var/lib/ceph/$service";
$path = '/var/log/ceph' if $service eq 'logs';
if ($dir_exists) {
my $err;
File::Path::remove_tree($path, {
keep_root => 1,
error => \$err,
});
warn "Error removing path, '$path'\n" if @$err;
}
}
my $path = "/var/lib/ceph/$service";
$path = '/var/log/ceph' if $service eq 'logs';
if ($dir_exists) {
my $err;
File::Path::remove_tree(
$path,
{
keep_root => 1,
error => \$err,
},
);
warn "Error removing path, '$path'\n" if @$err;
}
}
}
if (scalar @$monlist > 0 && !$is_local_mon) {
warn "Foreign MON address in ceph.conf. Keeping config & keyrings\n"
warn "Foreign MON address in ceph.conf. Keeping config & keyrings\n";
} else {
print "Removing config & keyring files\n";
for my $file (%$config_files) {
unlink $file if (-e $file);
}
print "Removing config & keyring files\n";
for my $file (%$config_files) {
unlink $file if (-e $file);
}
}
}
@ -163,24 +166,24 @@ sub purge_all_ceph_services {
my ($services) = @_;
foreach my $service (keys %$services) {
my $type = $services->{$service};
next if (!%$type);
my $type = $services->{$service};
next if (!%$type);
foreach my $name (keys %$type) {
my $service_exists = $type->{$name}->{service};
foreach my $name (keys %$type) {
my $service_exists = $type->{$name}->{service};
if ($service_exists) {
eval { PVE::Ceph::Services::ceph_service_cmd('disable', "$service.$name") };
warn "Could not disable ceph-$service\@$name, error: $@\n" if $@;
if ($service_exists) {
eval { PVE::Ceph::Services::ceph_service_cmd('disable', "$service.$name") };
warn "Could not disable ceph-$service\@$name, error: $@\n" if $@;
eval { PVE::Ceph::Services::ceph_service_cmd('stop', "$service.$name") };
warn "Could not stop ceph-$service\@$name, error: $@\n" if $@;
}
}
eval { PVE::Ceph::Services::ceph_service_cmd('stop', "$service.$name") };
warn "Could not stop ceph-$service\@$name, error: $@\n" if $@;
}
}
}
}
sub ceph_install_flag_file { return '/run/pve-ceph-install-flag' };
sub ceph_install_flag_file { return '/run/pve-ceph-install-flag' }
sub check_ceph_installed {
my ($service, $noerr) = @_;
@ -189,21 +192,20 @@ sub check_ceph_installed {
# NOTE: the flag file is checked as on a new installation, the binary gets
# extracted by dpkg before the installation is finished
if (! -x $ceph_service->{$service} || -f ceph_install_flag_file()) {
die "binary not installed: $ceph_service->{$service}\n" if !$noerr;
return undef;
if (!-x $ceph_service->{$service} || -f ceph_install_flag_file()) {
die "binary not installed: $ceph_service->{$service}\n" if !$noerr;
return undef;
}
return 1;
}
sub check_ceph_configured {
check_ceph_inited();
die "ceph not fully configured - missing '$pve_ckeyring_path'\n"
if ! -f $pve_ckeyring_path;
if !-f $pve_ckeyring_path;
return 1;
}
@ -215,13 +217,13 @@ sub check_ceph_inited {
my @errors;
push(@errors, "missing '$pve_ceph_cfgpath'") if ! -f $pve_ceph_cfgpath;
push(@errors, "missing '$pve_ceph_cfgdir'") if ! -d $pve_ceph_cfgdir;
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;
my $err = 'pveceph configuration not initialized - ' . join(', ', @errors) . "\n";
die $err if !$noerr;
return undef;
}
return 1;
@ -232,9 +234,9 @@ sub check_ceph_enabled {
return undef if !check_ceph_inited($noerr);
if (! -f $ceph_cfgpath) {
die "pveceph configuration not enabled\n" if !$noerr;
return undef;
if (!-f $ceph_cfgpath) {
die "pveceph configuration not enabled\n" if !$noerr;
return undef;
}
return 1;
@ -245,19 +247,19 @@ my $set_pool_setting = sub {
my $command;
if ($setting eq 'application') {
$command = {
prefix => "osd pool application enable",
pool => "$pool",
app => "$value",
};
$command = {
prefix => "osd pool application enable",
pool => "$pool",
app => "$value",
};
} else {
$command = {
prefix => "osd pool set",
pool => "$pool",
var => "$setting",
val => "$value",
format => 'plain',
};
$command = {
prefix => "osd pool set",
pool => "$pool",
var => "$setting",
val => "$value",
format => 'plain',
};
}
$rados = PVE::RADOS->new() if !$rados;
@ -271,42 +273,42 @@ sub set_pool {
my $rados = PVE::RADOS->new();
if (get_pool_type($pool, $rados) eq 'erasure') {
#remove parameters that cannot be changed for erasure coded pools
my $ignore_params = ['size', 'crush_rule'];
for my $setting (@$ignore_params) {
if ($param->{$setting}) {
print "cannot set '${setting}' for erasure coded pool\n";
delete $param->{$setting};
}
}
#remove parameters that cannot be changed for erasure coded pools
my $ignore_params = ['size', 'crush_rule'];
for my $setting (@$ignore_params) {
if ($param->{$setting}) {
print "cannot set '${setting}' for erasure coded pool\n";
delete $param->{$setting};
}
}
}
# by default, pool size always resets min_size, so set it as first item
# https://tracker.ceph.com/issues/44862
my $keys = [ grep { $_ ne 'size' } sort keys %$param ];
my $keys = [grep { $_ ne 'size' } sort keys %$param];
unshift @$keys, 'size' if exists $param->{size};
my $current_properties = get_pool_properties($pool, $rados);
for my $setting (@$keys) {
my $value = $param->{$setting};
my $value = $param->{$setting};
if (defined($current_properties->{$setting}) && $value eq $current_properties->{$setting}) {
print "skipping '${setting}', did not change\n";
delete $param->{$setting};
next;
}
if (defined($current_properties->{$setting}) && $value eq $current_properties->{$setting}) {
print "skipping '${setting}', did not change\n";
delete $param->{$setting};
next;
}
print "pool $pool: applying $setting = $value\n";
if (my $err = $set_pool_setting->($pool, $setting, $value, $rados)) {
print "$err";
} else {
delete $param->{$setting};
}
print "pool $pool: applying $setting = $value\n";
if (my $err = $set_pool_setting->($pool, $setting, $value, $rados)) {
print "$err";
} else {
delete $param->{$setting};
}
}
if (scalar(keys %$param) > 0) {
my $missing = join(', ', sort keys %$param );
die "Could not set: $missing\n";
my $missing = join(', ', sort keys %$param);
die "Could not set: $missing\n";
}
}
@ -315,10 +317,10 @@ sub get_pool_properties {
my ($pool, $rados) = @_;
$rados = PVE::RADOS->new() if !defined($rados);
my $command = {
prefix => "osd pool get",
pool => "$pool",
var => "all",
format => 'json',
prefix => "osd pool get",
pool => "$pool",
var => "all",
format => 'json',
};
return $rados->mon_command($command);
}
@ -337,14 +339,14 @@ sub create_pool {
my $pg_num = $param->{pg_num} || 128;
my $mon_params = {
prefix => "osd pool create",
pool => $pool,
pg_num => int($pg_num),
format => 'plain',
prefix => "osd pool create",
pool => $pool,
pg_num => int($pg_num),
format => 'plain',
};
$mon_params->{pool_type} = extract_param($param, 'pool_type') if $param->{pool_type};
$mon_params->{erasure_code_profile} = extract_param($param, 'erasure_code_profile')
if $param->{erasure_code_profile};
if $param->{erasure_code_profile};
$rados->mon_command($mon_params);
@ -367,11 +369,11 @@ sub destroy_pool {
# fixme: '--yes-i-really-really-mean-it'
$rados->mon_command({
prefix => "osd pool delete",
pool => $pool,
pool2 => $pool,
'yes_i_really_really_mean_it' => JSON::true,
format => 'plain',
prefix => "osd pool delete",
pool => $pool,
pool2 => $pool,
'yes_i_really_really_mean_it' => JSON::true,
format => 'plain',
});
}
@ -396,15 +398,15 @@ sub create_fs {
my ($fs, $param, $rados) = @_;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$rados = PVE::RADOS->new();
}
$rados->mon_command({
prefix => "fs new",
fs_name => $fs,
metadata => $param->{pool_metadata},
data => $param->{pool_data},
format => 'plain',
prefix => "fs new",
fs_name => $fs,
metadata => $param->{pool_metadata},
data => $param->{pool_data},
format => 'plain',
});
}
@ -413,23 +415,23 @@ sub destroy_fs {
$rados = PVE::RADOS->new() if !defined($rados);
$rados->mon_command({
prefix => "fs rm",
fs_name => $fs,
'yes_i_really_mean_it' => JSON::true,
format => 'plain',
prefix => "fs rm",
fs_name => $fs,
'yes_i_really_mean_it' => JSON::true,
format => 'plain',
});
}
sub setup_pve_symlinks {
# fail if we find a real file instead of a link
if (-f $ceph_cfgpath) {
my $lnk = readlink($ceph_cfgpath);
die "file '$ceph_cfgpath' already exists and is not a symlink to $pve_ceph_cfgpath\n"
if !$lnk || $lnk ne $pve_ceph_cfgpath;
my $lnk = readlink($ceph_cfgpath);
die "file '$ceph_cfgpath' already exists and is not a symlink to $pve_ceph_cfgpath\n"
if !$lnk || $lnk ne $pve_ceph_cfgpath;
} else {
mkdir $ceph_cfgdir;
symlink($pve_ceph_cfgpath, $ceph_cfgpath) ||
die "unable to create symlink '$ceph_cfgpath' - $!\n";
mkdir $ceph_cfgdir;
symlink($pve_ceph_cfgpath, $ceph_cfgpath)
|| die "unable to create symlink '$ceph_cfgpath' - $!\n";
}
my $ceph_uid = getpwnam('ceph');
my $ceph_gid = getgrnam('ceph');
@ -437,18 +439,18 @@ sub setup_pve_symlinks {
}
sub get_or_create_admin_keyring {
if (! -f $pve_ckeyring_path) {
run_command("ceph-authtool --create-keyring $pve_ckeyring_path " .
"--gen-key -n client.admin " .
"--cap mon 'allow *' " .
"--cap osd 'allow *' " .
"--cap mds 'allow *' " .
"--cap mgr 'allow *' ");
# we do not want to overwrite it
if (! -f $ckeyring_path) {
run_command("cp $pve_ckeyring_path $ckeyring_path");
run_command("chown ceph:ceph $ckeyring_path");
}
if (!-f $pve_ckeyring_path) {
run_command("ceph-authtool --create-keyring $pve_ckeyring_path "
. "--gen-key -n client.admin "
. "--cap mon 'allow *' "
. "--cap osd 'allow *' "
. "--cap mds 'allow *' "
. "--cap mgr 'allow *' ");
# we do not want to overwrite it
if (!-f $ckeyring_path) {
run_command("cp $pve_ckeyring_path $ckeyring_path");
run_command("chown ceph:ceph $ckeyring_path");
}
}
return $pve_ckeyring_path;
}
@ -458,29 +460,29 @@ sub create_or_update_crash_keyring_file {
my ($rados) = @_;
if (!defined($rados)) {
$rados = PVE::RADOS->new();
$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',
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);
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;
}
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;
PVE::Tools::file_set_contents($pve_ceph_crash_key_path, $output);
return 1;
}
return 0;
@ -491,11 +493,11 @@ sub ceph_volume_list {
my $result = {};
if (!check_ceph_installed('ceph_volume', 1)) {
return $result;
return $result;
}
my $output = '';
my $cmd = [ $ceph_service->{ceph_volume}, 'lvm', 'list', '--format', 'json' ];
my $cmd = [$ceph_service->{ceph_volume}, 'lvm', 'list', '--format', 'json'];
run_command($cmd, outfunc => sub { $output .= shift });
$result = eval { decode_json($output) };
@ -508,7 +510,7 @@ sub ceph_volume_zap {
die "no osdid given\n" if !defined($osdid);
my $cmd = [ $ceph_service->{ceph_volume}, 'lvm', 'zap', '--osd-id', $osdid ];
my $cmd = [$ceph_service->{ceph_volume}, 'lvm', 'zap', '--osd-id', $osdid];
push @$cmd, '--destroy' if $destroy;
run_command($cmd);
@ -520,84 +522,88 @@ sub get_db_wal_sizes {
my $rados = PVE::RADOS->new();
my $db_config = $rados->mon_command({ prefix => 'config-key dump', key => 'config/' });
$res->{db} = $db_config->{"config/osd/bluestore_block_db_size"} //
$db_config->{"config/global/bluestore_block_db_size"};
$res->{db} = $db_config->{"config/osd/bluestore_block_db_size"}
// $db_config->{"config/global/bluestore_block_db_size"};
$res->{wal} = $db_config->{"config/osd/bluestore_block_wal_size"} //
$db_config->{"config/global/bluestore_block_wal_size"};
$res->{wal} = $db_config->{"config/osd/bluestore_block_wal_size"}
// $db_config->{"config/global/bluestore_block_wal_size"};
if (!$res->{db} || !$res->{wal}) {
my $cfg = cfs_read_file('ceph.conf');
if (!$res->{db}) {
$res->{db} = $cfg->{osd}->{bluestore_block_db_size} //
$cfg->{global}->{bluestore_block_db_size};
}
my $cfg = cfs_read_file('ceph.conf');
if (!$res->{db}) {
$res->{db} = $cfg->{osd}->{bluestore_block_db_size}
// $cfg->{global}->{bluestore_block_db_size};
}
if (!$res->{wal}) {
$res->{wal} = $cfg->{osd}->{bluestore_block_wal_size} //
$cfg->{global}->{bluestore_block_wal_size};
}
if (!$res->{wal}) {
$res->{wal} = $cfg->{osd}->{bluestore_block_wal_size}
// $cfg->{global}->{bluestore_block_wal_size};
}
}
return $res;
}
sub get_possible_osd_flags {
my $possible_flags = {
pause => {
description => 'Pauses read and writes.',
type => 'boolean',
optional=> 1,
},
noup => {
description => 'OSDs are not allowed to start.',
type => 'boolean',
optional=> 1,
},
nodown => {
description => 'OSD failure reports are being ignored, such that the monitors will not mark OSDs down.',
type => 'boolean',
optional=> 1,
},
noout => {
description => 'OSDs will not automatically be marked out after the configured interval.',
type => 'boolean',
optional=> 1,
},
noin => {
description => 'OSDs that were previously marked out will not be marked back in when they start.',
type => 'boolean',
optional=> 1,
},
nobackfill => {
description => 'Backfilling of PGs is suspended.',
type => 'boolean',
optional=> 1,
},
norebalance => {
description => 'Rebalancing of PGs is suspended.',
type => 'boolean',
optional=> 1,
},
norecover => {
description => 'Recovery of PGs is suspended.',
type => 'boolean',
optional=> 1,
},
noscrub => {
description => 'Scrubbing is disabled.',
type => 'boolean',
optional=> 1,
},
'nodeep-scrub' => {
description => 'Deep Scrubbing is disabled.',
type => 'boolean',
optional=> 1,
},
notieragent => {
description => 'Cache tiering activity is suspended.',
type => 'boolean',
optional=> 1,
},
pause => {
description => 'Pauses read and writes.',
type => 'boolean',
optional => 1,
},
noup => {
description => 'OSDs are not allowed to start.',
type => 'boolean',
optional => 1,
},
nodown => {
description =>
'OSD failure reports are being ignored, such that the monitors will not mark OSDs down.',
type => 'boolean',
optional => 1,
},
noout => {
description =>
'OSDs will not automatically be marked out after the configured interval.',
type => 'boolean',
optional => 1,
},
noin => {
description =>
'OSDs that were previously marked out will not be marked back in when they start.',
type => 'boolean',
optional => 1,
},
nobackfill => {
description => 'Backfilling of PGs is suspended.',
type => 'boolean',
optional => 1,
},
norebalance => {
description => 'Rebalancing of PGs is suspended.',
type => 'boolean',
optional => 1,
},
norecover => {
description => 'Recovery of PGs is suspended.',
type => 'boolean',
optional => 1,
},
noscrub => {
description => 'Scrubbing is disabled.',
type => 'boolean',
optional => 1,
},
'nodeep-scrub' => {
description => 'Deep Scrubbing is disabled.',
type => 'boolean',
optional => 1,
},
notieragent => {
description => 'Cache tiering activity is suspended.',
type => 'boolean',
optional => 1,
},
};
return $possible_flags;
}
@ -608,7 +614,7 @@ sub get_real_flag_name {
# the 'pause' flag gets always set to both 'pauserd' and 'pausewr'
# so decide that the 'pause' flag is set if we detect 'pauserd'
my $flagmap = {
'pause' => 'pauserd',
'pause' => 'pauserd',
};
return $flagmap->{$flag} // $flag;
@ -622,8 +628,8 @@ sub ceph_cluster_status {
$status->{health} = $rados->mon_command({ prefix => 'health', detail => 'detail' });
if (!exists $status->{monmap}->{mons}) { # octopus moved most info out of status, re-add
$status->{monmap} = $rados->mon_command({ prefix => 'mon dump' });
$status->{mgrmap} = $rados->mon_command({ prefix => 'mgr dump' });
$status->{monmap} = $rados->mon_command({ prefix => 'mon dump' });
$status->{mgrmap} = $rados->mon_command({ prefix => 'mgr dump' });
}
return $status;
@ -646,17 +652,15 @@ sub create_ecprofile {
$failure_domain = 'host' if !$failure_domain;
my $profile = [
"crush-failure-domain=${failure_domain}",
"k=${k}",
"m=${m}",
"crush-failure-domain=${failure_domain}", "k=${k}", "m=${m}",
];
push(@$profile, "crush-device-class=${device_class}") if $device_class;
$rados->mon_command({
prefix => 'osd erasure-code-profile set',
name => $name,
profile => $profile,
prefix => 'osd erasure-code-profile set',
name => $name,
profile => $profile,
});
}
@ -665,9 +669,9 @@ sub destroy_ecprofile {
$rados = PVE::RADOS->new() if !$rados;
my $command = {
prefix => 'osd erasure-code-profile rm',
name => $profile,
format => 'plain',
prefix => 'osd erasure-code-profile rm',
name => $profile,
format => 'plain',
};
return $rados->mon_command($command);
}
@ -682,9 +686,9 @@ sub destroy_crush_rule {
$rados = PVE::RADOS->new() if !$rados;
my $command = {
prefix => 'osd crush rule rm',
name => $rule,
format => 'plain',
prefix => 'osd crush rule rm',
name => $rule,
format => 'plain',
};
return $rados->mon_command($command);
}

View file

@ -18,37 +18,36 @@ sub update_cert_cache {
my ($update_node, $clear) = @_;
syslog('info', "Clearing outdated entries from certificate cache")
if $clear;
if $clear;
$cert_cache_timestamp = time() if !defined($update_node);
my $node_list = defined($update_node) ?
[ $update_node ] : [ keys %$cert_cache_nodes ];
my $node_list = defined($update_node) ? [$update_node] : [keys %$cert_cache_nodes];
foreach my $node (@$node_list) {
my $clear_old = sub {
if (my $old_fp = $cert_cache_nodes->{$node}) {
# distrust old fingerprint
delete $cert_cache_fingerprints->{$old_fp};
# ensure reload on next proxied request
delete $cert_cache_nodes->{$node};
}
};
my $clear_old = sub {
if (my $old_fp = $cert_cache_nodes->{$node}) {
# distrust old fingerprint
delete $cert_cache_fingerprints->{$old_fp};
# ensure reload on next proxied request
delete $cert_cache_nodes->{$node};
}
};
my $fp = eval { PVE::Cluster::get_node_fingerprint($node) };
if (my $err = $@) {
warn "$err\n";
&$clear_old() if $clear;
next;
}
my $fp = eval { PVE::Cluster::get_node_fingerprint($node) };
if (my $err = $@) {
warn "$err\n";
&$clear_old() if $clear;
next;
}
my $old_fp = $cert_cache_nodes->{$node};
$cert_cache_fingerprints->{$fp} = 1;
$cert_cache_nodes->{$node} = $fp;
my $old_fp = $cert_cache_nodes->{$node};
$cert_cache_fingerprints->{$fp} = 1;
$cert_cache_nodes->{$node} = $fp;
if (defined($old_fp) && $fp ne $old_fp) {
delete $cert_cache_fingerprints->{$old_fp};
}
if (defined($old_fp) && $fp ne $old_fp) {
delete $cert_cache_fingerprints->{$old_fp};
}
}
}
@ -57,33 +56,36 @@ sub initialize_cert_cache {
my ($node) = @_;
update_cert_cache($node)
if defined($node) && !defined($cert_cache_nodes->{$node});
if defined($node) && !defined($cert_cache_nodes->{$node});
}
sub check_cert_fingerprint {
my ($cert) = @_;
# clear cache every 30 minutes at least
update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60*30;
update_cert_cache(undef, 1) if time() - $cert_cache_timestamp >= 60 * 30;
# get fingerprint of server certificate
my $fp = Net::SSLeay::X509_get_fingerprint($cert, 'sha256');
return 0 if !defined($fp) || $fp eq ''; # error
my $check = sub {
for my $expected (keys %$cert_cache_fingerprints) {
return 1 if $fp eq $expected;
}
return 0;
for my $expected (keys %$cert_cache_fingerprints) {
return 1 if $fp eq $expected;
}
return 0;
};
return 1 if &$check();
# clear cache and retry at most once every minute
if (time() - $cert_cache_timestamp >= 60) {
syslog ('info', "Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache");
update_cert_cache();
return &$check();
syslog(
'info',
"Could not verify remote node certificate '$fp' with list of pinned certificates, refreshing cache",
);
update_cert_cache();
return &$check();
}
return 0;

View file

@ -9,26 +9,35 @@ use PVE::Tools;
my $account_prefix = '/etc/pve/priv/acme';
PVE::JSONSchema::register_standard_option('pve-acme-account-name', {
description => 'ACME account config file name.',
type => 'string',
format => 'pve-configid',
format_description => 'name',
optional => 1,
default => 'default',
});
PVE::JSONSchema::register_standard_option(
'pve-acme-account-name',
{
description => 'ACME account config file name.',
type => 'string',
format => 'pve-configid',
format_description => 'name',
optional => 1,
default => 'default',
},
);
PVE::JSONSchema::register_standard_option('pve-acme-account-contact', {
type => 'string',
format => 'email-list',
description => 'Contact email addresses.',
});
PVE::JSONSchema::register_standard_option(
'pve-acme-account-contact',
{
type => 'string',
format => 'email-list',
description => 'Contact email addresses.',
},
);
PVE::JSONSchema::register_standard_option('pve-acme-directory-url', {
type => 'string',
description => 'URL of ACME CA directory endpoint.',
pattern => '^https?://.*',
});
PVE::JSONSchema::register_standard_option(
'pve-acme-directory-url',
{
type => 'string',
description => 'URL of ACME CA directory endpoint.',
pattern => '^https?://.*',
},
);
my $local_cert_lock = '/var/lock/pve-certs.lock';
@ -61,30 +70,30 @@ sub set_cert_files {
my $key_path_tmp = "${path_prefix}.key.old";
die "Custom certificate file exists but force flag is not set.\n"
if !$force && -e $cert_path;
if !$force && -e $cert_path;
die "Custom certificate key file exists but force flag is not set.\n"
if !$force && -e $key_path;
if !$force && -e $key_path;
PVE::Tools::file_copy($cert_path, $cert_path_tmp) if -e $cert_path;
PVE::Tools::file_copy($key_path, $key_path_tmp) if -e $key_path;
eval {
PVE::Tools::file_set_contents($cert_path, $cert);
PVE::Tools::file_set_contents($key_path, $key) if $key;
$info = PVE::Certificate::get_certificate_info($cert_path);
PVE::Tools::file_set_contents($cert_path, $cert);
PVE::Tools::file_set_contents($key_path, $key) if $key;
$info = PVE::Certificate::get_certificate_info($cert_path);
};
my $err = $@;
if ($err) {
if (-e $cert_path_tmp && -e $key_path_tmp) {
eval {
warn "Attempting to restore old certificate files..\n";
PVE::Tools::file_copy($cert_path_tmp, $cert_path);
PVE::Tools::file_copy($key_path_tmp, $key_path);
};
warn "$@\n" if $@;
}
die "Setting certificate files failed - $err\n"
if (-e $cert_path_tmp && -e $key_path_tmp) {
eval {
warn "Attempting to restore old certificate files..\n";
PVE::Tools::file_copy($cert_path_tmp, $cert_path);
PVE::Tools::file_copy($key_path_tmp, $key_path);
};
warn "$@\n" if $@;
}
die "Setting certificate files failed - $err\n";
}
unlink $cert_path_tmp;
@ -100,14 +109,18 @@ sub acme_account_dir {
sub list_acme_accounts {
my $accounts = [];
return $accounts if ! -d $account_prefix;
return $accounts if !-d $account_prefix;
PVE::Tools::dir_glob_foreach($account_prefix, qr/[^.]+.*/, sub {
my ($name) = @_;
PVE::Tools::dir_glob_foreach(
$account_prefix,
qr/[^.]+.*/,
sub {
my ($name) = @_;
push @$accounts, $name
if PVE::JSONSchema::pve_verify_configid($name, 1);
});
push @$accounts, $name
if PVE::JSONSchema::pve_verify_configid($name, 1);
},
);
return $accounts;
}

View file

@ -14,12 +14,12 @@ PVE::Status::Plugin->init();
sub foreach_plug($&) {
my ($status_cfg, $code) = @_;
for my $id (sort keys %{$status_cfg->{ids}}) {
my $plugin_config = $status_cfg->{ids}->{$id};
next if $plugin_config->{disable};
for my $id (sort keys %{ $status_cfg->{ids} }) {
my $plugin_config = $status_cfg->{ids}->{$id};
next if $plugin_config->{disable};
my $plugin = PVE::Status::Plugin->lookup($plugin_config->{type});
$code->($plugin, $id, $plugin_config);
my $plugin = PVE::Status::Plugin->lookup($plugin_config->{type});
$code->($plugin, $id, $plugin_config);
}
}
@ -29,9 +29,9 @@ sub update_all($$@) {
my $method = "update_${subsystem}_status";
for my $txn (@$transactions) {
my $plugin = PVE::Status::Plugin->lookup($txn->{cfg}->{type});
my $plugin = PVE::Status::Plugin->lookup($txn->{cfg}->{type});
$plugin->$method($txn, @params);
$plugin->$method($txn, @params);
}
}
@ -46,18 +46,22 @@ sub transactions_start {
my $transactions = [];
foreach_plug($cfg, sub {
my ($plugin, $id, $plugin_config) = @_;
foreach_plug(
$cfg,
sub {
my ($plugin, $id, $plugin_config) = @_;
my $connection = $plugin->_connect($plugin_config, $id);
my $connection = $plugin->_connect($plugin_config, $id);
push @$transactions, {
connection => $connection,
cfg => $plugin_config,
id => $id,
data => '',
};
});
push @$transactions,
{
connection => $connection,
cfg => $plugin_config,
id => $id,
data => '',
};
},
);
return $transactions;
}
@ -66,16 +70,16 @@ sub transactions_finish {
my ($transactions) = @_;
for my $txn (@$transactions) {
my $plugin = PVE::Status::Plugin->lookup($txn->{cfg}->{type});
my $plugin = PVE::Status::Plugin->lookup($txn->{cfg}->{type});
eval { $plugin->flush_data($txn) };
my $flush_err = $@;
warn "$flush_err" if $flush_err;
eval { $plugin->flush_data($txn) };
my $flush_err = $@;
warn "$flush_err" if $flush_err;
$plugin->_disconnect($txn->{connection}, $txn->{cfg});
$txn->{connection} = undef;
# avoid log spam, already got a send error; disconnect would fail too
warn "disconnect failed: $@" if $@ && !$flush_err;
$plugin->_disconnect($txn->{connection}, $txn->{cfg});
$txn->{connection} = undef;
# avoid log spam, already got a send error; disconnect would fail too
warn "disconnect failed: $@" if $@ && !$flush_err;
}
}

View file

@ -28,8 +28,8 @@ sub new {
my $self = $class->SUPER::new(%args);
$self->{rpcenv} = PVE::RPCEnvironment->init(
$self->{trusted_env} ? 'priv' : 'pub',
atfork => sub { $self->atfork_handler() },
$self->{trusted_env} ? 'priv' : 'pub',
atfork => sub { $self->atfork_handler() },
);
return $self;
@ -67,11 +67,13 @@ sub auth_handler {
my $require_auth = 1;
# explicitly allow some calls without auth
if (($rel_uri eq '/access/domains' && $method eq 'GET') ||
($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST')) ||
($rel_uri eq '/access/openid/login' && $method eq 'POST') ||
($rel_uri eq '/access/openid/auth-url' && $method eq 'POST')) {
$require_auth = 0;
if (
($rel_uri eq '/access/domains' && $method eq 'GET')
|| ($rel_uri eq '/access/ticket' && ($method eq 'GET' || $method eq 'POST'))
|| ($rel_uri eq '/access/openid/login' && $method eq 'POST')
|| ($rel_uri eq '/access/openid/auth-url' && $method eq 'POST')
) {
$require_auth = 0;
}
my ($username, $age);
@ -79,50 +81,50 @@ sub auth_handler {
my $isUpload = 0;
if ($require_auth) {
if ($api_token) {
# the token-ID `<user>@<realm>!<tokenname>` is the user for token based authentication
$username = PVE::AccessControl::verify_token($api_token);
} else {
die "No ticket\n" if !$ticket;
if ($api_token) {
# the token-ID `<user>@<realm>!<tokenname>` is the user for token based authentication
$username = PVE::AccessControl::verify_token($api_token);
} else {
die "No ticket\n" if !$ticket;
($username, $age, my $tfa_info) = PVE::AccessControl::verify_ticket($ticket);
$rpcenv->check_user_enabled($username);
($username, $age, my $tfa_info) = PVE::AccessControl::verify_ticket($ticket);
$rpcenv->check_user_enabled($username);
if (defined($tfa_info)) {
if (defined(my $challenge = $tfa_info->{challenge})) {
$rpcenv->set_u2f_challenge($challenge);
}
die "No ticket\n" if ($rel_uri ne '/access/tfa' || $method ne 'POST');
}
}
if (defined($tfa_info)) {
if (defined(my $challenge = $tfa_info->{challenge})) {
$rpcenv->set_u2f_challenge($challenge);
}
die "No ticket\n" if ($rel_uri ne '/access/tfa' || $method ne 'POST');
}
}
$rpcenv->set_user($username);
$rpcenv->set_user($username);
if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
my ($node, $storeid) = ($1, $2);
# CSRF check are omitted if $isUpload is set, so check user upload permission here
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
$rpcenv->check_api2_permissions($perm, $username, {});
$isUpload = 1;
}
if ($method eq 'POST' && $rel_uri =~ m|^/nodes/([^/]+)/storage/([^/]+)/upload$|) {
my ($node, $storeid) = ($1, $2);
# CSRF check are omitted if $isUpload is set, so check user upload permission here
my $perm = { check => ['perm', "/storage/$storeid", ['Datastore.AllocateTemplate']] };
$rpcenv->check_api2_permissions($perm, $username, {});
$isUpload = 1;
}
# Skip CSRF check for file upload (difficult to pass CSRF header with native html forms).
# Also skip the check with API tokens, as one of the design goals of API tokens was to
# provide stateless API access without requiring round-trips to get such CSRF tokens.
# CSRF-prevention also does not make much sense outside of the browser context.
if ($method ne 'GET' && !($api_token || $isUpload)) {
my $euid = $>;
PVE::AccessControl::verify_csrf_prevention_token($username, $token) if $euid != 0;
}
# Skip CSRF check for file upload (difficult to pass CSRF header with native html forms).
# Also skip the check with API tokens, as one of the design goals of API tokens was to
# provide stateless API access without requiring round-trips to get such CSRF tokens.
# CSRF-prevention also does not make much sense outside of the browser context.
if ($method ne 'GET' && !($api_token || $isUpload)) {
my $euid = $>;
PVE::AccessControl::verify_csrf_prevention_token($username, $token) if $euid != 0;
}
}
return {
ticket => $ticket,
token => $token,
userid => $username,
age => $age,
isUpload => $isUpload,
api_token => $api_token,
ticket => $ticket,
token => $token,
userid => $username,
age => $age,
isUpload => $isUpload,
api_token => $api_token,
};
}
@ -132,78 +134,83 @@ sub rest_handler {
my $rpcenv = $self->{rpcenv};
my $resp = {
status => HTTP_NOT_IMPLEMENTED,
message => "Method '$method $rel_uri' not implemented",
status => HTTP_NOT_IMPLEMENTED,
message => "Method '$method $rel_uri' not implemented",
};
my ($handler, $info);
eval {
my $uri_param = {};
($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
return if !$handler || !$info;
my $uri_param = {};
($handler, $info) = PVE::API2->find_handler($method, $rel_uri, $uri_param);
return if !$handler || !$info;
for my $p (sort keys %{$params}) {
if (defined($uri_param->{$p}) && $uri_param->{$p} ne $params->{$p}) {
raise_param_exc({
$p => "duplicate parameter (already defined in URI) with conflicting values!"
});
}
$uri_param->{$p} = $params->{$p};
}
for my $p (sort keys %{$params}) {
if (defined($uri_param->{$p}) && $uri_param->{$p} ne $params->{$p}) {
raise_param_exc({
$p =>
"duplicate parameter (already defined in URI) with conflicting values!",
});
}
$uri_param->{$p} = $params->{$p};
}
raise_perm_exc("URI '$rel_uri' not available with API token, need proper ticket.\n")
if $auth->{api_token} && !$info->{allowtoken};
raise_perm_exc("URI '$rel_uri' not available with API token, need proper ticket.\n")
if $auth->{api_token} && !$info->{allowtoken};
# check access permissions
$rpcenv->check_api2_permissions($info->{permissions}, $auth->{userid}, $uri_param);
# check access permissions
$rpcenv->check_api2_permissions($info->{permissions}, $auth->{userid}, $uri_param);
if ($info->{proxyto} || $info->{proxyto_callback}) {
my $node = PVE::API2Tools::resolve_proxyto(
$rpcenv, $info->{proxyto_callback}, $info->{proxyto}, $uri_param);
if ($info->{proxyto} || $info->{proxyto_callback}) {
my $node = PVE::API2Tools::resolve_proxyto(
$rpcenv,
$info->{proxyto_callback},
$info->{proxyto},
$uri_param,
);
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
die "unable to proxy file uploads" if $auth->{isUpload};
my $remip = $self->remote_node_ip($node);
$resp = { proxy => $remip, proxynode => $node, proxy_params => $params };
return;
}
}
if ($node ne 'localhost' && $node ne PVE::INotify::nodename()) {
die "unable to proxy file uploads" if $auth->{isUpload};
my $remip = $self->remote_node_ip($node);
$resp = { proxy => $remip, proxynode => $node, proxy_params => $params };
return;
}
}
my $euid = $>;
if ($info->{protected} && ($euid != 0)) {
$resp = { proxy => 'localhost' , proxy_params => $params };
return;
}
my $euid = $>;
if ($info->{protected} && ($euid != 0)) {
$resp = { proxy => 'localhost', proxy_params => $params };
return;
}
$resp = {
data => $handler->handle($info, $uri_param),
info => $info, # useful to format output
status => HTTP_OK,
};
$resp = {
data => $handler->handle($info, $uri_param),
info => $info, # useful to format output
status => HTTP_OK,
};
if (my $count = $rpcenv->get_result_attrib('total')) {
$resp->{total} = $count;
}
if (my $count = $rpcenv->get_result_attrib('total')) {
$resp->{total} = $count;
}
if (my $diff = $rpcenv->get_result_attrib('changes')) {
$resp->{changes} = $diff;
}
if (my $diff = $rpcenv->get_result_attrib('changes')) {
$resp->{changes} = $diff;
}
};
my $err = $@;
$rpcenv->set_user(undef); # clear after request
if ($err) {
$resp = { info => $info };
if (ref($err) eq "PVE::Exception") {
$resp->{status} = $err->{code} || HTTP_INTERNAL_SERVER_ERROR;
$resp->{errors} = $err->{errors} if $err->{errors};
$resp->{message} = $err->{msg};
} else {
$resp->{status} = HTTP_INTERNAL_SERVER_ERROR;
$resp->{message} = $err;
}
$resp = { info => $info };
if (ref($err) eq "PVE::Exception") {
$resp->{status} = $err->{code} || HTTP_INTERNAL_SERVER_ERROR;
$resp->{errors} = $err->{errors} if $err->{errors};
$resp->{message} = $err->{msg};
} else {
$resp->{status} = HTTP_INTERNAL_SERVER_ERROR;
$resp->{message} = $err;
}
}
return $resp;

View file

@ -41,37 +41,41 @@ my $saved_config_props = [qw(enabled schedule)];
sub detect_changed_runtime_props {
my ($jobid, $type, $cfg) = @_;
lock_job_state($jobid, $type, sub {
my $old_state = read_job_state($jobid, $type) // $default_state;
lock_job_state(
$jobid,
$type,
sub {
my $old_state = read_job_state($jobid, $type) // $default_state;
my $updated = 0;
for my $prop (@$saved_config_props) {
my $old_prop = $old_state->{config}->{$prop} // '';
my $new_prop = $cfg->{$prop} // '';
next if "$old_prop" eq "$new_prop";
my $updated = 0;
for my $prop (@$saved_config_props) {
my $old_prop = $old_state->{config}->{$prop} // '';
my $new_prop = $cfg->{$prop} // '';
next if "$old_prop" eq "$new_prop";
if (defined($cfg->{$prop})) {
$old_state->{config}->{$prop} = $cfg->{$prop};
} else {
delete $old_state->{config}->{$prop};
}
if (defined($cfg->{$prop})) {
$old_state->{config}->{$prop} = $cfg->{$prop};
} else {
delete $old_state->{config}->{$prop};
}
$updated = 1;
}
$updated = 1;
}
return if !$updated;
$old_state->{updated} = time();
return if !$updated;
$old_state->{updated} = time();
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($old_state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($old_state));
},
);
}
# lockless, since we use file_get_contents, which is atomic
sub read_job_state {
my ($jobid, $type) = @_;
my $path = $get_state_file->($jobid, $type);
return if ! -e $path;
return if !-e $path;
my $raw = PVE::Tools::file_get_contents($path);
@ -79,7 +83,7 @@ sub read_job_state {
# untaint $raw
if ($raw =~ m/^(\{.*\})$/) {
return decode_json($1);
return decode_json($1);
}
die "invalid json data in '$path'\n";
@ -100,16 +104,16 @@ my $get_job_task_status = sub {
my ($state) = @_;
if (!defined($state->{upid})) {
return; # not started
return; # not started
}
my ($task, $filename) = PVE::Tools::upid_decode($state->{upid}, 1);
die "unable to parse worker upid - $state->{upid}\n" if !$task;
die "no such task\n" if ! -f $filename;
die "no such task\n" if !-f $filename;
my $pstart = PVE::ProcFSTools::read_proc_starttime($task->{pid});
if ($pstart && $pstart == $task->{pstart}) {
return; # still running
return; # still running
}
return PVE::Tools::upid_read_status($state->{upid});
@ -125,24 +129,28 @@ sub update_job_stopped {
return if !defined($state) || $state->{state} ne 'started'; # removed or not started
if (defined($get_job_task_status->($state))) {
lock_job_state($jobid, $type, sub {
my $state = read_job_state($jobid, $type);
return if !defined($state) || $state->{state} ne 'started'; # removed or not started
lock_job_state(
$jobid,
$type,
sub {
my $state = read_job_state($jobid, $type);
return if !defined($state) || $state->{state} ne 'started'; # removed or not started
my $new_state = {
state => 'stopped',
msg => $get_job_task_status->($state) // 'internal error',
upid => $state->{upid},
config => $state->{config},
};
my $new_state = {
state => 'stopped',
msg => $get_job_task_status->($state) // 'internal error',
upid => $state->{upid},
config => $state->{config},
};
if ($state->{updated}) { # save updated time stamp
$new_state->{updated} = $state->{updated};
}
if ($state->{updated}) { # save updated time stamp
$new_state->{updated} = $state->{updated};
}
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
},
);
}
}
@ -150,23 +158,27 @@ sub update_job_stopped {
sub create_job {
my ($jobid, $type, $cfg) = @_;
lock_job_state($jobid, $type, sub {
my $state = read_job_state($jobid, $type) // $default_state;
lock_job_state(
$jobid,
$type,
sub {
my $state = read_job_state($jobid, $type) // $default_state;
if ($state->{state} ne 'created') {
die "job state already exists\n";
}
if ($state->{state} ne 'created') {
die "job state already exists\n";
}
$state->{time} = time();
for my $prop (@$saved_config_props) {
if (defined($cfg->{$prop})) {
$state->{config}->{$prop} = $cfg->{$prop};
}
}
$state->{time} = time();
for my $prop (@$saved_config_props) {
if (defined($cfg->{$prop})) {
$state->{config}->{$prop} = $cfg->{$prop};
}
}
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($state));
},
);
}
# to be called when the job is removed
@ -185,61 +197,73 @@ sub starting_job {
my $state = read_job_state($jobid, $type);
return 0 if !defined($state) || $state->{state} eq 'started'; # removed or already started
lock_job_state($jobid, $type, sub {
my $state = read_job_state($jobid, $type);
return 0 if !defined($state) || $state->{state} eq 'started'; # removed or already started
lock_job_state(
$jobid,
$type,
sub {
my $state = read_job_state($jobid, $type);
return 0 if !defined($state) || $state->{state} eq 'started'; # removed or already started
my $new_state = {
state => 'starting',
time => time(),
config => $state->{config},
};
my $new_state = {
state => 'starting',
time => time(),
config => $state->{config},
};
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
},
);
return 1;
}
sub started_job {
my ($jobid, $type, $upid, $msg) = @_;
lock_job_state($jobid, $type, sub {
my $state = read_job_state($jobid, $type);
return if !defined($state); # job was removed, do not update
die "unexpected state '$state->{state}'\n" if $state->{state} ne 'starting';
lock_job_state(
$jobid,
$type,
sub {
my $state = read_job_state($jobid, $type);
return if !defined($state); # job was removed, do not update
die "unexpected state '$state->{state}'\n" if $state->{state} ne 'starting';
my $new_state;
if (defined($msg)) {
$new_state = {
state => 'stopped',
msg => $msg,
time => time(),
};
} else {
$new_state = {
state => 'started',
upid => $upid,
};
}
$new_state->{config} = $state->{config};
my $new_state;
if (defined($msg)) {
$new_state = {
state => 'stopped',
msg => $msg,
time => time(),
};
} else {
$new_state = {
state => 'started',
upid => $upid,
};
}
$new_state->{config} = $state->{config};
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($new_state));
},
);
}
# will be called when the job schedule is updated
sub update_last_runtime {
my ($jobid, $type) = @_;
lock_job_state($jobid, $type, sub {
my $old_state = read_job_state($jobid, $type) // $default_state;
lock_job_state(
$jobid,
$type,
sub {
my $old_state = read_job_state($jobid, $type) // $default_state;
$old_state->{updated} = time();
$old_state->{updated} = time();
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($old_state));
});
my $path = $get_state_file->($jobid, $type);
PVE::Tools::file_set_contents($path, encode_json($old_state));
},
);
}
sub get_last_runtime {
@ -250,9 +274,9 @@ sub get_last_runtime {
return $state->{updated} if defined($state->{updated});
if (my $upid = $state->{upid}) {
my ($task) = PVE::Tools::upid_decode($upid, 1);
die "unable to parse worker upid\n" if !$task;
return $task->{starttime};
my ($task) = PVE::Tools::upid_decode($upid, 1);
die "unable to parse worker upid\n" if !$task;
return $task->{starttime};
}
return $state->{time} // 0;
@ -266,77 +290,85 @@ sub run_jobs {
my $jobs_cfg = cfs_read_file('jobs.cfg');
my $nodename = PVE::INotify::nodename();
foreach my $id (sort keys %{$jobs_cfg->{ids}}) {
my $cfg = $jobs_cfg->{ids}->{$id};
my $type = $cfg->{type};
my $schedule = delete $cfg->{schedule};
foreach my $id (sort keys %{ $jobs_cfg->{ids} }) {
my $cfg = $jobs_cfg->{ids}->{$id};
my $type = $cfg->{type};
my $schedule = delete $cfg->{schedule};
# only schedule local jobs
next if defined($cfg->{node}) && $cfg->{node} ne $nodename;
# only schedule local jobs
next if defined($cfg->{node}) && $cfg->{node} ne $nodename;
eval { update_job_stopped($id, $type) };
if (my $err = $@) {
warn "could not update job state, skipping - $err\n";
next;
}
eval { update_job_stopped($id, $type) };
if (my $err = $@) {
warn "could not update job state, skipping - $err\n";
next;
}
# update last runtime on the first run when 'repeat-missed' is 0, so that a missed job
# will not start immediately after boot
update_last_runtime($id, $type) if $first_run && !$cfg->{'repeat-missed'};
# update last runtime on the first run when 'repeat-missed' is 0, so that a missed job
# will not start immediately after boot
update_last_runtime($id, $type) if $first_run && !$cfg->{'repeat-missed'};
next if defined($cfg->{enabled}) && !$cfg->{enabled}; # only schedule actually enabled jobs
next if defined($cfg->{enabled}) && !$cfg->{enabled}; # only schedule actually enabled jobs
my $last_run = get_last_runtime($id, $type);
my $calspec = PVE::CalendarEvent::parse_calendar_event($schedule);
my $next_sync = PVE::CalendarEvent::compute_next_event($calspec, $last_run);
my $last_run = get_last_runtime($id, $type);
my $calspec = PVE::CalendarEvent::parse_calendar_event($schedule);
my $next_sync = PVE::CalendarEvent::compute_next_event($calspec, $last_run);
next if !defined($next_sync) || time() < $next_sync; # not yet its (next) turn
next if !defined($next_sync) || time() < $next_sync; # not yet its (next) turn
my $plugin = PVE::Job::Registry->lookup($type);
if (starting_job($id, $type)) {
PVE::Cluster::cfs_update();
my $plugin = PVE::Job::Registry->lookup($type);
if (starting_job($id, $type)) {
PVE::Cluster::cfs_update();
my $upid = eval { $plugin->run($cfg, $id, $schedule) };
if (my $err = $@) {
warn $@ if $@;
started_job($id, $type, undef, $err);
} elsif ($upid eq 'OK') { # some jobs return OK immediately
started_job($id, $type, undef, 'OK');
} else {
started_job($id, $type, $upid);
}
}
my $upid = eval { $plugin->run($cfg, $id, $schedule) };
if (my $err = $@) {
warn $@ if $@;
started_job($id, $type, undef, $err);
} elsif ($upid eq 'OK') { # some jobs return OK immediately
started_job($id, $type, undef, 'OK');
} else {
started_job($id, $type, $upid);
}
}
}
}
# creates and removes statefiles for job configs
sub synchronize_job_states_with_config {
cfs_lock_file('jobs.cfg', undef, sub {
my $data = cfs_read_file('jobs.cfg');
cfs_lock_file(
'jobs.cfg',
undef,
sub {
my $data = cfs_read_file('jobs.cfg');
for my $id (keys $data->{ids}->%*) {
my $job = $data->{ids}->{$id};
my $type = $job->{type};
for my $id (keys $data->{ids}->%*) {
my $job = $data->{ids}->{$id};
my $type = $job->{type};
my $path = $get_state_file->($id, $type);
if (-e $path) {
detect_changed_runtime_props($id, $type, $job);
} else {
create_job($id, $type, $job);
}
}
my $path = $get_state_file->($id, $type);
if (-e $path) {
detect_changed_runtime_props($id, $type, $job);
} else {
create_job($id, $type, $job);
}
}
my $valid_types = PVE::Job::Registry->lookup_types();
my $type_regex = join("|", $valid_types->@*);
my $valid_types = PVE::Job::Registry->lookup_types();
my $type_regex = join("|", $valid_types->@*);
PVE::Tools::dir_glob_foreach($state_dir, "(${type_regex})-(.*).json", sub {
my ($path, $type, $id) = @_;
PVE::Tools::dir_glob_foreach(
$state_dir,
"(${type_regex})-(.*).json",
sub {
my ($path, $type, $id) = @_;
if (!defined($data->{ids}->{$id})) {
remove_job($id, $type);
}
});
});
if (!defined($data->{ids}->{$id})) {
remove_job($id, $type);
}
},
);
},
);
die $@ if $@;
}

View file

@ -17,17 +17,17 @@ sub run {
my $props = $class->properties();
# remove all non vzdump related options
foreach my $opt (keys %$conf) {
delete $conf->{$opt} if !defined($props->{$opt});
delete $conf->{$opt} if !defined($props->{$opt});
}
$conf->{'job-id'} = $job_id;
# Required as string parameters # FIXME why?! we could just check ref()
for my $key (keys $PVE::VZDump::Common::PROPERTY_STRINGS->%*) {
if ($conf->{$key} && ref($conf->{$key}) eq 'HASH') {
my $format = $PVE::VZDump::Common::PROPERTY_STRINGS->{$key};
$conf->{$key} = PVE::JSONSchema::print_property_string($conf->{$key}, $format);
}
if ($conf->{$key} && ref($conf->{$key}) eq 'HASH') {
my $format = $PVE::VZDump::Common::PROPERTY_STRINGS->{$key};
$conf->{$key} = PVE::JSONSchema::print_property_string($conf->{$key}, $format);
}
}
$conf->{quiet} = 1; # do not write to stdout/stderr

View file

@ -15,25 +15,31 @@ my $MAXDOMAINS = 5;
my $node_config_lock = '/var/lock/pvenode.lock';
PVE::JSONSchema::register_format('pve-acme-domain', sub {
my ($domain, $noerr) = @_;
PVE::JSONSchema::register_format(
'pve-acme-domain',
sub {
my ($domain, $noerr) = @_;
my $label = qr/[a-z0-9][a-z0-9_-]*/i;
my $label = qr/[a-z0-9][a-z0-9_-]*/i;
return $domain if $domain =~ /^$label(?:\.$label)+$/;
return undef if $noerr;
die "value '$domain' does not look like a valid domain name!\n";
});
return $domain if $domain =~ /^$label(?:\.$label)+$/;
return undef if $noerr;
die "value '$domain' does not look like a valid domain name!\n";
},
);
PVE::JSONSchema::register_format('pve-acme-alias', sub {
my ($alias, $noerr) = @_;
PVE::JSONSchema::register_format(
'pve-acme-alias',
sub {
my ($alias, $noerr) = @_;
my $label = qr/[a-z0-9_][a-z0-9_-]*/i;
my $label = qr/[a-z0-9_][a-z0-9_-]*/i;
return $alias if $alias =~ /^$label(?:\.$label)+$/;
return undef if $noerr;
die "value '$alias' does not look like a valid alias name!\n";
});
return $alias if $alias =~ /^$label(?:\.$label)+$/;
return undef if $noerr;
die "value '$alias' does not look like a valid alias name!\n";
},
);
sub config_file {
my ($node) = @_;
@ -66,8 +72,8 @@ sub lock_config {
# make sure configuration file is up-to-date
my $code = sub {
PVE::Cluster::cfs_update();
$realcode->(@_);
PVE::Cluster::cfs_update();
$realcode->(@_);
};
my $res = lock_file($node_config_lock, 10, $code, @param);
@ -79,53 +85,54 @@ sub lock_config {
my $confdesc = {
description => {
type => 'string',
description => "Description for the Node. Shown in the web-interface node notes panel."
." This is saved as comment inside the configuration file.",
maxLength => 64 * 1024,
optional => 1,
type => 'string',
description => "Description for the Node. Shown in the web-interface node notes panel."
. " This is saved as comment inside the configuration file.",
maxLength => 64 * 1024,
optional => 1,
},
'startall-onboot-delay' => {
description => 'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
type => 'integer',
minimum => 0,
maximum => 300,
default => 0,
optional => 1,
description =>
'Initial delay in seconds, before starting all the Virtual Guests with on-boot enabled.',
type => 'integer',
minimum => 0,
maximum => 300,
default => 0,
optional => 1,
},
'ballooning-target' => {
description => 'RAM usage target for ballooning (in percent of total memory)',
type => 'integer',
minimum => 0,
maximum => 100,
default => 80,
optional => 1,
description => 'RAM usage target for ballooning (in percent of total memory)',
type => 'integer',
minimum => 0,
maximum => 100,
default => 80,
optional => 1,
},
};
my $wakeonlan_desc = {
mac => {
type => 'string',
description => 'MAC address for wake on LAN',
format => 'mac-addr',
format_description => 'MAC address',
default_key => 1,
type => 'string',
description => 'MAC address for wake on LAN',
format => 'mac-addr',
format_description => 'MAC address',
default_key => 1,
},
'bind-interface' => {
type => 'string',
description => 'Bind to this interface when sending wake on LAN packet',
default => 'The interface carrying the default route',
format => 'pve-iface',
format_description => 'bind interface',
optional => 1,
type => 'string',
description => 'Bind to this interface when sending wake on LAN packet',
default => 'The interface carrying the default route',
format => 'pve-iface',
format_description => 'bind interface',
optional => 1,
},
'broadcast-address' => {
type => 'string',
description => 'IPv4 broadcast address to use when sending wake on LAN packet',
default => '255.255.255.255',
format => 'ipv4',
format_description => 'IPv4 broadcast address',
optional => 1,
type => 'string',
description => 'IPv4 broadcast address to use when sending wake on LAN packet',
default => '255.255.255.255',
format => 'ipv4',
format_description => 'IPv4 broadcast address',
optional => 1,
},
};
@ -138,37 +145,37 @@ $confdesc->{wakeonlan} = {
my $acme_domain_desc = {
domain => {
type => 'string',
format => 'pve-acme-domain',
format_description => 'domain',
description => 'domain for this node\'s ACME certificate',
default_key => 1,
type => 'string',
format => 'pve-acme-domain',
format_description => 'domain',
description => 'domain for this node\'s ACME certificate',
default_key => 1,
},
plugin => {
type => 'string',
format => 'pve-configid',
description => 'The ACME plugin ID',
format_description => 'name of the plugin configuration',
optional => 1,
default => 'standalone',
type => 'string',
format => 'pve-configid',
description => 'The ACME plugin ID',
format_description => 'name of the plugin configuration',
optional => 1,
default => 'standalone',
},
alias => {
type => 'string',
format => 'pve-acme-alias',
format_description => 'domain',
description => 'Alias for the Domain to verify ACME Challenge over DNS',
optional => 1,
type => 'string',
format => 'pve-acme-alias',
format_description => 'domain',
description => 'Alias for the Domain to verify ACME Challenge over DNS',
optional => 1,
},
};
my $acmedesc = {
account => get_standard_option('pve-acme-account-name'),
domains => {
type => 'string',
format => 'pve-acme-domain-list',
format_description => 'domain[;domain;...]',
description => 'List of domains for this node\'s ACME certificate',
optional => 1,
type => 'string',
format => 'pve-acme-domain-list',
format_description => 'domain[;domain;...]',
description => 'List of domains for this node\'s ACME certificate',
optional => 1,
},
};
@ -179,14 +186,14 @@ $confdesc->{acme} = {
optional => 1,
};
for my $i (0..$MAXDOMAINS) {
for my $i (0 .. $MAXDOMAINS) {
$confdesc->{"acmedomain$i"} = {
type => 'string',
description => 'ACME domain and validation plugin',
format => $acme_domain_desc,
optional => 1,
type => 'string',
description => 'ACME domain and validation plugin',
format => $acme_domain_desc,
optional => 1,
};
};
}
my $conf_schema = {
type => 'object',
@ -212,17 +219,17 @@ sub write_node_config {
# add description as comment to top of file
my $descr = $conf->{description} || '';
foreach my $cl (split(/\n/, $descr)) {
$raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
$raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
}
for my $key (sort keys %$conf) {
next if ($key eq 'description');
next if ($key eq 'digest');
next if ($key eq 'description');
next if ($key eq 'digest');
my $value = $conf->{$key};
die "detected invalid newline inside property '$key'\n"
if $value =~ m/\n/;
$raw .= "$key: $value\n";
my $value = $conf->{$key};
die "detected invalid newline inside property '$key'\n"
if $value =~ m/\n/;
$raw .= "$key: $value\n";
}
return $raw;
@ -235,10 +242,10 @@ sub get_wakeonlan_config {
my $res = {};
if (defined($node_conf->{wakeonlan})) {
$res = eval {
PVE::JSONSchema::parse_property_string($wakeonlan_desc, $node_conf->{wakeonlan})
};
die $@ if $@;
$res = eval {
PVE::JSONSchema::parse_property_string($wakeonlan_desc, $node_conf->{wakeonlan});
};
die $@ if $@;
}
return $res;
@ -254,55 +261,52 @@ sub get_acme_conf {
my $res = {};
if (defined($node_conf->{acme})) {
$res = eval {
PVE::JSONSchema::parse_property_string($acmedesc, $node_conf->{acme})
};
if (my $err = $@) {
return undef if $noerr;
die $err;
}
my $standalone_domains = delete($res->{domains}) // '';
$res->{domains} = {};
for my $domain (split(";", $standalone_domains)) {
$domain = lc($domain);
die "duplicate domain '$domain' in ACME config properties\n"
if defined($res->{domains}->{$domain});
$res = eval { PVE::JSONSchema::parse_property_string($acmedesc, $node_conf->{acme}) };
if (my $err = $@) {
return undef if $noerr;
die $err;
}
my $standalone_domains = delete($res->{domains}) // '';
$res->{domains} = {};
for my $domain (split(";", $standalone_domains)) {
$domain = lc($domain);
die "duplicate domain '$domain' in ACME config properties\n"
if defined($res->{domains}->{$domain});
$res->{domains}->{$domain}->{plugin} = 'standalone';
$res->{domains}->{$domain}->{_configkey} = 'acme';
}
$res->{domains}->{$domain}->{plugin} = 'standalone';
$res->{domains}->{$domain}->{_configkey} = 'acme';
}
}
$res->{account} //= 'default';
for my $index (0..$MAXDOMAINS) {
my $domain_rec = $node_conf->{"acmedomain$index"};
next if !defined($domain_rec);
for my $index (0 .. $MAXDOMAINS) {
my $domain_rec = $node_conf->{"acmedomain$index"};
next if !defined($domain_rec);
my $parsed = eval {
PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec)
};
if (my $err = $@) {
return undef if $noerr;
die $err;
}
my $domain = lc(delete $parsed->{domain});
if (my $exists = $res->{domains}->{$domain}) {
return undef if $noerr;
die "duplicate domain '$domain' in ACME config properties"
." 'acmedomain$index' and '$exists->{_configkey}'\n";
}
$parsed->{plugin} //= 'standalone';
my $parsed =
eval { PVE::JSONSchema::parse_property_string($acme_domain_desc, $domain_rec) };
if (my $err = $@) {
return undef if $noerr;
die $err;
}
my $domain = lc(delete $parsed->{domain});
if (my $exists = $res->{domains}->{$domain}) {
return undef if $noerr;
die "duplicate domain '$domain' in ACME config properties"
. " 'acmedomain$index' and '$exists->{_configkey}'\n";
}
$parsed->{plugin} //= 'standalone';
my $plugin_id = $parsed->{plugin};
if ($plugin_id ne 'standalone') {
my $plugins = PVE::API2::ACMEPlugin::load_config();
die "plugin '$plugin_id' for domain '$domain' not found!\n"
if !$plugins->{ids}->{$plugin_id};
}
my $plugin_id = $parsed->{plugin};
if ($plugin_id ne 'standalone') {
my $plugins = PVE::API2::ACMEPlugin::load_config();
die "plugin '$plugin_id' for domain '$domain' not found!\n"
if !$plugins->{ids}->{$plugin_id};
}
$parsed->{_configkey} = "acmedomain$index";
$res->{domains}->{$domain} = $parsed;
$parsed->{_configkey} = "acmedomain$index";
$res->{domains}->{$domain} = $parsed;
}
return $res;

View file

@ -12,17 +12,18 @@ my $cache;
my $get_cache = sub {
if (!defined($cache)) {
my $uid = getpwnam('root');
my $gid = getgrnam('www-data');
my $uid = getpwnam('root');
my $gid = getgrnam('www-data');
$cache = Proxmox::RS::SharedCache->new({
path => "/run/pve/metrics",
owner => $uid,
group => $gid,
entry_mode => 0640, # Entry permissions
keep_old => OLD_GENERATIONS,
}
);
$cache = Proxmox::RS::SharedCache->new(
{
path => "/run/pve/metrics",
owner => $uid,
group => $gid,
entry_mode => 0640, # Entry permissions
keep_old => OLD_GENERATIONS,
},
);
}
return $cache;
@ -55,24 +56,24 @@ my sub gauge {
my ($id, $timestamp, $metric, $value) = @_;
return {
metric => $metric,
id => $id,
value => $value + 0,
timestamp => $timestamp + 0,
type => 'gauge',
}
metric => $metric,
id => $id,
value => $value + 0,
timestamp => $timestamp + 0,
type => 'gauge',
};
}
my sub derive {
my ($id, $timestamp, $metric, $value) = @_;
return {
metric => $metric,
id => $id,
value => $value + 0,
timestamp => $timestamp + 0,
type => 'derive',
}
metric => $metric,
id => $id,
value => $value + 0,
timestamp => $timestamp + 0,
type => 'derive',
};
}
my $nodename = PVE::INotify::nodename();
@ -91,8 +92,8 @@ my sub get_node_metrics {
my ($netin, $netout) = (0, 0);
for my $dev (grep { /^$PVE::Network::PHYSICAL_NIC_RE$/ } keys $data->{nics}->%*) {
$netin += $data->{nics}->{$dev}->{receive};
$netout += $data->{nics}->{$dev}->{transmit};
$netin += $data->{nics}->{$dev}->{receive};
$netout += $data->{nics}->{$dev}->{transmit};
}
push @$metrics, derive($id, $timestamp, "net_in", $netin);
push @$metrics, derive($id, $timestamp, "net_out", $netout);
@ -127,24 +128,24 @@ my sub get_qemu_metrics {
my $timestamp = $stats->{timestamp};
for my $vmid (keys $stats->{data}->%*) {
my $id = "qemu/$vmid";
my $guest_data = $stats->{data}->{$vmid};
my $id = "qemu/$vmid";
my $guest_data = $stats->{data}->{$vmid};
if ($guest_data->{status} eq 'running') {
push @$metrics, gauge($id, $timestamp, "cpu_current", $guest_data->{cpu});
push @$metrics, gauge($id, $timestamp, "mem_used", $guest_data->{mem});
push @$metrics, derive($id, $timestamp, "disk_read", $guest_data->{diskread});
push @$metrics, derive($id, $timestamp, "disk_write", $guest_data->{diskwrite});
push @$metrics, derive($id, $timestamp, "net_in", $guest_data->{netin});
push @$metrics, derive($id, $timestamp, "net_out", $guest_data->{netout});
}
if ($guest_data->{status} eq 'running') {
push @$metrics, gauge($id, $timestamp, "cpu_current", $guest_data->{cpu});
push @$metrics, gauge($id, $timestamp, "mem_used", $guest_data->{mem});
push @$metrics, derive($id, $timestamp, "disk_read", $guest_data->{diskread});
push @$metrics, derive($id, $timestamp, "disk_write", $guest_data->{diskwrite});
push @$metrics, derive($id, $timestamp, "net_in", $guest_data->{netin});
push @$metrics, derive($id, $timestamp, "net_out", $guest_data->{netout});
}
push @$metrics, gauge($id, $timestamp, "uptime", $guest_data->{uptime});
push @$metrics, gauge($id, $timestamp, "cpu_max", $guest_data->{cpus});
push @$metrics, gauge($id, $timestamp, "mem_total", $guest_data->{maxmem});
push @$metrics, gauge($id, $timestamp, "disk_total", $guest_data->{maxdisk});
# TODO: This one always seems to be 0?
# push @$metrics, num_metric("disk_used", $id, $guest_data->{disk}, $timestamp);
push @$metrics, gauge($id, $timestamp, "uptime", $guest_data->{uptime});
push @$metrics, gauge($id, $timestamp, "cpu_max", $guest_data->{cpus});
push @$metrics, gauge($id, $timestamp, "mem_total", $guest_data->{maxmem});
push @$metrics, gauge($id, $timestamp, "disk_total", $guest_data->{maxdisk});
# TODO: This one always seems to be 0?
# push @$metrics, num_metric("disk_used", $id, $guest_data->{disk}, $timestamp);
}
return $metrics;
@ -158,23 +159,23 @@ my sub get_lxc_metrics {
my $timestamp = $stats->{timestamp};
for my $vmid (keys $stats->{data}->%*) {
my $id = "lxc/$vmid";
my $guest_data = $stats->{data}->{$vmid};
my $id = "lxc/$vmid";
my $guest_data = $stats->{data}->{$vmid};
if ($guest_data->{status} eq 'running') {
push @$metrics, gauge($id, $timestamp, "cpu_current", $guest_data->{cpu});
push @$metrics, gauge($id, $timestamp, "mem_used", $guest_data->{mem});
push @$metrics, derive($id, $timestamp, "disk_read", $guest_data->{diskread});
push @$metrics, derive($id, $timestamp, "disk_write", $guest_data->{diskwrite});
push @$metrics, derive($id, $timestamp, "net_in", $guest_data->{netin});
push @$metrics, derive($id, $timestamp, "net_out", $guest_data->{netout});
push @$metrics, gauge($id, $timestamp, "disk_used", $guest_data->{disk});
}
if ($guest_data->{status} eq 'running') {
push @$metrics, gauge($id, $timestamp, "cpu_current", $guest_data->{cpu});
push @$metrics, gauge($id, $timestamp, "mem_used", $guest_data->{mem});
push @$metrics, derive($id, $timestamp, "disk_read", $guest_data->{diskread});
push @$metrics, derive($id, $timestamp, "disk_write", $guest_data->{diskwrite});
push @$metrics, derive($id, $timestamp, "net_in", $guest_data->{netin});
push @$metrics, derive($id, $timestamp, "net_out", $guest_data->{netout});
push @$metrics, gauge($id, $timestamp, "disk_used", $guest_data->{disk});
}
push @$metrics, gauge($id, $timestamp, "uptime", $guest_data->{uptime});
push @$metrics, gauge($id, $timestamp, "cpu_max", $guest_data->{cpus});
push @$metrics, gauge($id, $timestamp, "mem_total", $guest_data->{maxmem});
push @$metrics, gauge($id, $timestamp, "disk_total", $guest_data->{maxdisk});
push @$metrics, gauge($id, $timestamp, "uptime", $guest_data->{uptime});
push @$metrics, gauge($id, $timestamp, "cpu_max", $guest_data->{cpus});
push @$metrics, gauge($id, $timestamp, "mem_total", $guest_data->{maxmem});
push @$metrics, gauge($id, $timestamp, "disk_total", $guest_data->{maxdisk});
}
return $metrics;
@ -188,11 +189,11 @@ my sub get_storage_metrics {
my $timestamp = $stats->{timestamp};
for my $sid (keys $stats->{data}->%*) {
my $id = "storage/$nodename/$sid";
my $data = $stats->{data}->{$sid};
my $id = "storage/$nodename/$sid";
my $data = $stats->{data}->{$sid};
push @$metrics, gauge($id, $timestamp, "disk_total", $data->{total});
push @$metrics, gauge($id, $timestamp, "disk_used", $data->{used});
push @$metrics, gauge($id, $timestamp, "disk_total", $data->{total});
push @$metrics, gauge($id, $timestamp, "disk_used", $data->{used});
}
return $metrics;
@ -213,10 +214,10 @@ sub get_local_metrics {
my $data = $get_cache->()->get_last($history);
for my $stat_gen ($data->@*) {
push @$metrics, get_node_metrics($stat_gen->{node})->@*;
push @$metrics, get_qemu_metrics($stat_gen->{qemu})->@*;
push @$metrics, get_lxc_metrics($stat_gen->{lxc})->@*;
push @$metrics, get_storage_metrics($stat_gen->{storage})->@*;
push @$metrics, get_node_metrics($stat_gen->{node})->@*;
push @$metrics, get_qemu_metrics($stat_gen->{qemu})->@*;
push @$metrics, get_lxc_metrics($stat_gen->{lxc})->@*;
push @$metrics, get_storage_metrics($stat_gen->{storage})->@*;
}
return $metrics;

View file

@ -11,12 +11,16 @@ my sub dir2text {
print STDERR "dir2text '${target_dir}${regexp}'...";
my $text = "# output '${target_dir}${regexp}' file(s)\n";
PVE::Tools::dir_glob_foreach($target_dir, $regexp, sub {
my ($file) = @_;
return if $file eq '.' || $file eq '..';
$text .= "\n# cat $target_dir$file\n";
$text .= PVE::Tools::file_get_contents($target_dir.$file)."\n";
});
PVE::Tools::dir_glob_foreach(
$target_dir,
$regexp,
sub {
my ($file) = @_;
return if $file eq '.' || $file eq '..';
$text .= "\n# cat $target_dir$file\n";
$text .= PVE::Tools::file_get_contents($target_dir . $file) . "\n";
},
);
return $text;
}
@ -25,147 +29,143 @@ my sub cmd_exists { system("command -v '$_[0]' > /dev/null 2>&1") == 0 }
my $init_report_cmds = sub {
my $report_def = {
general => {
title => 'general system info',
order => 10,
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') },
'apt-cache policy | grep -vP "^ +origin "',
'apt-mark showhold',
'lscpu',
'pvesh get /cluster/resources --type node --output-format=yaml',
],
},
'system-load' => {
title => 'overall system load info',
order => 20,
cmds => [
'top -b -c -w512 -n 1 -o TIME | head -n 30',
'head /proc/pressure/*',
],
},
storage => {
order => 30,
cmds => [
'cat /etc/pve/storage.cfg',
'pvesm status',
'cat /etc/fstab',
'findmnt --ascii',
'df --human -T',
'proxmox-boot-tool status',
],
},
'virtual guests' => {
order => 40,
cmds => [
'qm list',
sub { dir2text('/etc/pve/qemu-server/', '\d+\.conf') },
'pct list',
sub { dir2text('/etc/pve/lxc/', '\d+\.conf') },
],
},
network => {
order => 45,
cmds => [
'ip -details -statistics address',
'ip -details -4 route show',
'ip -details -6 route show',
'cat /etc/network/interfaces',
sub { dir2text('/etc/network/interfaces.d/', '.*') },
'cat /etc/pve/sdn/.running-config',
sub { dir2text('/etc/pve/sdn/', '.+\.cfg') },
sub { dir2text('/etc/pve/sdn/', '.+\.json') },
],
},
firewall => {
order => 50,
cmds => [
sub { dir2text('/etc/pve/firewall/', '.+\.fw') },
'cat /etc/pve/local/host.fw',
sub { dir2text('/etc/pve/sdn/firewall/', '.+\.fw') },
'iptables-save -c | column -t -l4 -o" "',
],
},
cluster => {
order => 60,
cmds => [
'pvecm nodes',
'pvecm status',
'cat /etc/pve/corosync.conf 2>/dev/null',
'ha-manager status',
'cat /etc/pve/datacenter.cfg',
],
},
jobs => {
order => 65,
cmds => [
'cat /etc/pve/jobs.cfg',
],
},
hardware => {
order => 70,
cmds => [
'dmidecode -t bios',
'lspci -nnk',
],
},
'block devices' => {
order => 80,
cmds => [
'lsblk --ascii -M -o +HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN,WWN',
'ls -l /dev/disk/by-*/',
'iscsiadm -m node',
'iscsiadm -m session',
],
},
volumes => {
order => 90,
cmds => [
'pvs',
'lvs',
'vgs',
],
},
general => {
title => 'general system info',
order => 10,
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') },
'apt-cache policy | grep -vP "^ +origin "',
'apt-mark showhold',
'lscpu',
'pvesh get /cluster/resources --type node --output-format=yaml',
],
},
'system-load' => {
title => 'overall system load info',
order => 20,
cmds => [
'top -b -c -w512 -n 1 -o TIME | head -n 30', 'head /proc/pressure/*',
],
},
storage => {
order => 30,
cmds => [
'cat /etc/pve/storage.cfg',
'pvesm status',
'cat /etc/fstab',
'findmnt --ascii',
'df --human -T',
'proxmox-boot-tool status',
],
},
'virtual guests' => {
order => 40,
cmds => [
'qm list',
sub { dir2text('/etc/pve/qemu-server/', '\d+\.conf') },
'pct list',
sub { dir2text('/etc/pve/lxc/', '\d+\.conf') },
],
},
network => {
order => 45,
cmds => [
'ip -details -statistics address',
'ip -details -4 route show',
'ip -details -6 route show',
'cat /etc/network/interfaces',
sub { dir2text('/etc/network/interfaces.d/', '.*') },
'cat /etc/pve/sdn/.running-config',
sub { dir2text('/etc/pve/sdn/', '.+\.cfg') },
sub { dir2text('/etc/pve/sdn/', '.+\.json') },
],
},
firewall => {
order => 50,
cmds => [
sub { dir2text('/etc/pve/firewall/', '.+\.fw') },
'cat /etc/pve/local/host.fw',
sub { dir2text('/etc/pve/sdn/firewall/', '.+\.fw') },
'iptables-save -c | column -t -l4 -o" "',
],
},
cluster => {
order => 60,
cmds => [
'pvecm nodes',
'pvecm status',
'cat /etc/pve/corosync.conf 2>/dev/null',
'ha-manager status',
'cat /etc/pve/datacenter.cfg',
],
},
jobs => {
order => 65,
cmds => [
'cat /etc/pve/jobs.cfg',
],
},
hardware => {
order => 70,
cmds => [
'dmidecode -t bios', 'lspci -nnk',
],
},
'block devices' => {
order => 80,
cmds => [
'lsblk --ascii -M -o +HOTPLUG,ROTA,PHY-SEC,FSTYPE,MODEL,TRAN,WWN',
'ls -l /dev/disk/by-*/',
'iscsiadm -m node',
'iscsiadm -m session',
],
},
volumes => {
order => 90,
cmds => [
'pvs', 'lvs', 'vgs',
],
},
};
if (cmd_exists('zfs')) {
push @{$report_def->{volumes}->{cmds}},
'zpool status',
'zpool list -v',
'zfs list',
'arcstat',
;
push @{ $report_def->{volumes}->{cmds} },
'zpool status',
'zpool list -v',
'zfs list',
'arcstat',
;
}
if (-e '/etc/ceph/ceph.conf') {
push @{$report_def->{volumes}->{cmds}},
'pveceph status',
'ceph osd status',
'ceph df',
'ceph osd df tree',
'ceph device ls',
'cat /etc/ceph/ceph.conf',
'ceph config dump',
'pveceph pool ls',
'ceph versions',
'ceph health detail',
;
push @{ $report_def->{volumes}->{cmds} },
'pveceph status',
'ceph osd status',
'ceph df',
'ceph osd df tree',
'ceph device ls',
'cat /etc/ceph/ceph.conf',
'ceph config dump',
'pveceph pool ls',
'ceph versions',
'ceph health detail',
;
}
if (cmd_exists('multipath')) {
push @{$report_def->{disks}->{cmds}},
'cat /etc/multipath.conf',
'cat /etc/multipath/wwids',
'multipath -ll',
;
push @{ $report_def->{disks}->{cmds} },
'cat /etc/multipath.conf',
'cat /etc/multipath/wwids',
'multipath -ll',
;
}
return $report_def;
@ -176,40 +176,41 @@ sub generate {
my $report = '';
my $record_output = sub {
$report .= shift . "\n";
$report .= shift . "\n";
};
local $ENV{'PATH'} = '/sbin:/bin:/usr/sbin:/usr/bin';
my $cmd_timeout = 10; # generous timeout
my $run_cmd_params = {
outfunc => $record_output,
errfunc => $record_output,
timeout => $cmd_timeout,
noerr => 1, # avoid checking programs exit code
outfunc => $record_output,
errfunc => $record_output,
timeout => $cmd_timeout,
noerr => 1, # avoid checking programs exit code
};
my $sorter = sub { ($def->{$_[0]}->{order} // 1<<30) <=> ($def->{$_[1]}->{order} // 1<<30) };
my $sorter =
sub { ($def->{ $_[0] }->{order} // 1 << 30) <=> ($def->{ $_[1] }->{order} // 1 << 30) };
for my $section ( sort { $sorter->($a, $b) } keys %$def) {
my $s = $def->{$section};
my $title = $s->{title} // "info about $section";
for my $section (sort { $sorter->($a, $b) } keys %$def) {
my $s = $def->{$section};
my $title = $s->{title} // "info about $section";
$report .= "\n==== $title ====\n";
for my $command (@{$s->{cmds}}) {
eval {
if (ref $command eq 'CODE') {
$report .= PVE::Tools::run_with_timeout($cmd_timeout, $command);
} else {
print STDERR "Process ".$command."...";
$report .= "\n# $command\n";
PVE::Tools::run_command($command, %$run_cmd_params);
}
print STDERR "OK";
};
print STDERR "\n";
$report .= "\nERROR: $@\n" if $@;
}
$report .= "\n==== $title ====\n";
for my $command (@{ $s->{cmds} }) {
eval {
if (ref $command eq 'CODE') {
$report .= PVE::Tools::run_with_timeout($cmd_timeout, $command);
} else {
print STDERR "Process " . $command . "...";
$report .= "\n# $command\n";
PVE::Tools::run_command($command, %$run_cmd_params);
}
print STDERR "OK";
};
print STDERR "\n";
$report .= "\nERROR: $@\n" if $@;
}
}
return $report;

View file

@ -28,27 +28,27 @@ sub init {
my $accept_lock_fn = "/var/lock/pvedaemon.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $lockfh = IO::File->new(">>${accept_lock_fn}")
|| die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $socket = $self->create_reusable_socket(85, '127.0.0.1');
$self->{server_config} = {
keep_alive => 100,
max_conn => 500,
max_requests => 1000,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
trusted_env => 1,
keep_alive => 100,
max_conn => 500,
max_requests => 1000,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
trusted_env => 1,
};
}
sub run {
my ($self) = @_;
my $server = PVE::HTTPServer->new(%{$self->{server_config}});
my $server = PVE::HTTPServer->new(%{ $self->{server_config} });
$server->run();
}
@ -58,10 +58,10 @@ $daemon->register_stop_command();
$daemon->register_status_command();
our $cmddef = {
start => [ __PACKAGE__, 'start', []],
restart => [ __PACKAGE__, 'restart', []],
stop => [ __PACKAGE__, 'stop', []],
status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
start => [__PACKAGE__, 'start', []],
restart => [__PACKAGE__, 'restart', []],
stop => [__PACKAGE__, 'stop', []],
status => [__PACKAGE__, 'status', [], undef, sub { print shift . "\n"; }],
};
1;

View file

@ -67,8 +67,8 @@ sub init {
my $accept_lock_fn = "/var/lock/pveproxy.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $lockfh = IO::File->new(">>${accept_lock_fn}")
|| die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $listen_ip = $proxyconf->{LISTEN_IP};
my $socket = $self->create_reusable_socket(8006, $listen_ip);
@ -78,13 +78,13 @@ sub init {
add_dirs($dirs, '/novnc/' => "$basedirs->{novnc}/");
add_dirs($dirs, '/pve-docs/' => "$basedirs->{docs}/");
add_dirs($dirs, '/pve-docs/api-viewer/extjs/' => "$basedirs->{extjs}/");
add_dirs($dirs, '/pve2/css/' => "$basedirs->{manager}/css/");
add_dirs($dirs, '/pve2/css/' => "$basedirs->{manager}/css/");
add_dirs($dirs, '/pve2/ext6/', "$basedirs->{extjs}/");
add_dirs($dirs, '/pve2/fa/css/' => "$basedirs->{fontawesome}/css/");
add_dirs($dirs, '/pve2/fa/fonts/' => "$basedirs->{fontawesome}/fonts/");
add_dirs($dirs, '/pve2/font-logos/' => "$basedirs->{fontlogos}/");
add_dirs($dirs, '/pve2/images/' => "$basedirs->{manager}/images/");
add_dirs($dirs, '/pve2/js/' => "$basedirs->{manager}/js/");
add_dirs($dirs, '/pve2/fa/css/' => "$basedirs->{fontawesome}/css/");
add_dirs($dirs, '/pve2/fa/fonts/' => "$basedirs->{fontawesome}/fonts/");
add_dirs($dirs, '/pve2/font-logos/' => "$basedirs->{fontlogos}/");
add_dirs($dirs, '/pve2/images/' => "$basedirs->{manager}/images/");
add_dirs($dirs, '/pve2/js/' => "$basedirs->{manager}/js/");
add_dirs($dirs, '/pve2/locale/', "$basedirs->{i18n}/");
add_dirs($dirs, '/pve2/sencha-touch/', "$basedirs->{sencha_touch}/");
add_dirs($dirs, '/pve2/touch/', "$basedirs->{manager}/touch/");
@ -94,70 +94,73 @@ sub init {
add_dirs($dirs, '/xtermjs/' => "$basedirs->{xtermjs}/");
$self->{server_config} = {
title => 'Proxmox VE API',
keep_alive => 100,
max_conn => 500,
max_requests => 1000,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
trusted_env => 0, # not trusted, anyone can connect
logfile => '/var/log/pveproxy/access.log',
allow_from => $proxyconf->{ALLOW_FROM},
deny_from => $proxyconf->{DENY_FROM},
policy => $proxyconf->{POLICY},
ssl => {
cipher_list => $proxyconf->{CIPHERS},
ciphersuites => $proxyconf->{CIPHERSUITES},
key_file => '/etc/pve/local/pve-ssl.key',
cert_file => '/etc/pve/local/pve-ssl.pem',
honor_cipher_order => $proxyconf->{HONOR_CIPHER_ORDER},
},
compression => $proxyconf->{COMPRESSION},
proxy_real_ip_header => $proxyconf->{PROXY_REAL_IP_HEADER},
proxy_real_ip_allow_from => $proxyconf->{PROXY_REAL_IP_ALLOW_FROM},
# Note: there is no authentication for those pages and dirs!
pages => {
'/' => sub { get_index($self->{nodename}, @_) },
# avoid authentication when accessing favicon
'/favicon.ico' => {
file => "$basedirs->{manager}/images/favicon.ico",
},
'/proxmoxlib.js' => {
file => "$basedirs->{widgettoolkit}/proxmoxlib.js",
},
'/qrcode.min.js' => {
file => '/usr/share/javascript/qrcodejs/qrcode.min.js',
},
},
dirs => $dirs,
title => 'Proxmox VE API',
keep_alive => 100,
max_conn => 500,
max_requests => 1000,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
trusted_env => 0, # not trusted, anyone can connect
logfile => '/var/log/pveproxy/access.log',
allow_from => $proxyconf->{ALLOW_FROM},
deny_from => $proxyconf->{DENY_FROM},
policy => $proxyconf->{POLICY},
ssl => {
cipher_list => $proxyconf->{CIPHERS},
ciphersuites => $proxyconf->{CIPHERSUITES},
key_file => '/etc/pve/local/pve-ssl.key',
cert_file => '/etc/pve/local/pve-ssl.pem',
honor_cipher_order => $proxyconf->{HONOR_CIPHER_ORDER},
},
compression => $proxyconf->{COMPRESSION},
proxy_real_ip_header => $proxyconf->{PROXY_REAL_IP_HEADER},
proxy_real_ip_allow_from => $proxyconf->{PROXY_REAL_IP_ALLOW_FROM},
# Note: there is no authentication for those pages and dirs!
pages => {
'/' => sub { get_index($self->{nodename}, @_) },
# avoid authentication when accessing favicon
'/favicon.ico' => {
file => "$basedirs->{manager}/images/favicon.ico",
},
'/proxmoxlib.js' => {
file => "$basedirs->{widgettoolkit}/proxmoxlib.js",
},
'/qrcode.min.js' => {
file => '/usr/share/javascript/qrcodejs/qrcode.min.js',
},
},
dirs => $dirs,
};
if (defined($proxyconf->{DHPARAMS})) {
$self->{server_config}->{ssl}->{dh_file} = $proxyconf->{DHPARAMS};
$self->{server_config}->{ssl}->{dh_file} = $proxyconf->{DHPARAMS};
}
if (defined($proxyconf->{DISABLE_TLS_1_2})) {
$self->{server_config}->{ssl}->{tlsv1_2} = !$proxyconf->{DISABLE_TLS_1_2};
$self->{server_config}->{ssl}->{tlsv1_2} = !$proxyconf->{DISABLE_TLS_1_2};
}
if (defined($proxyconf->{DISABLE_TLS_1_3})) {
$self->{server_config}->{ssl}->{tlsv1_3} = !$proxyconf->{DISABLE_TLS_1_3};
$self->{server_config}->{ssl}->{tlsv1_3} = !$proxyconf->{DISABLE_TLS_1_3};
}
my $custom_key_path = '/etc/pve/local/pveproxy-ssl.key';
if (defined($proxyconf->{TLS_KEY_FILE})) {
$custom_key_path = $proxyconf->{TLS_KEY_FILE};
$custom_key_path = $proxyconf->{TLS_KEY_FILE};
}
if (-f '/etc/pve/local/pveproxy-ssl.pem' && -f $custom_key_path) {
$self->{server_config}->{ssl}->{cert_file} = '/etc/pve/local/pveproxy-ssl.pem';
$self->{server_config}->{ssl}->{key_file} = $custom_key_path;
syslog('info', 'Using \'/etc/pve/local/pveproxy-ssl.pem\' as certificate for the web interface.');
$self->{server_config}->{ssl}->{cert_file} = '/etc/pve/local/pveproxy-ssl.pem';
$self->{server_config}->{ssl}->{key_file} = $custom_key_path;
syslog(
'info',
'Using \'/etc/pve/local/pveproxy-ssl.pem\' as certificate for the web interface.',
);
}
}
sub run {
my ($self) = @_;
my $server = PVE::HTTPServer->new(%{$self->{server_config}});
my $server = PVE::HTTPServer->new(%{ $self->{server_config} });
$server->run();
}
@ -167,10 +170,10 @@ $daemon->register_stop_command();
$daemon->register_status_command();
our $cmddef = {
start => [ __PACKAGE__, 'start', []],
restart => [ __PACKAGE__, 'restart', []],
stop => [ __PACKAGE__, 'stop', []],
status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
start => [__PACKAGE__, 'start', []],
restart => [__PACKAGE__, 'restart', []],
stop => [__PACKAGE__, 'stop', []],
status => [__PACKAGE__, 'status', [], undef, sub { print shift . "\n"; }],
};
sub is_phone {
@ -181,8 +184,8 @@ sub is_phone {
return 1 if $ua =~ m/(iPhone|iPod|Windows Phone)/;
if ($ua =~ m/Mobile(\/|\s)/) {
return 1 if $ua =~ m/(BlackBerry|BB)/;
return 1 if ($ua =~ m/(Android)/) && ($ua !~ m/(Silk)/);
return 1 if $ua =~ m/(BlackBerry|BB)/;
return 1 if ($ua =~ m/(Android)/) && ($ua !~ m/(Silk)/);
}
return 0;
@ -200,38 +203,40 @@ sub get_index {
my $theme = "auto";
if (my $cookie = $r->header('Cookie')) {
if (my $newlang = ($cookie =~ /(?:^|\s)PVELangCookie=([^;]*)/)[0]) {
if ($newlang =~ m/^[a-z]{2,3}(_[A-Z]{2,3})?$/) {
$lang = $newlang;
}
}
if (my $newlang = ($cookie =~ /(?:^|\s)PVELangCookie=([^;]*)/)[0]) {
if ($newlang =~ m/^[a-z]{2,3}(_[A-Z]{2,3})?$/) {
$lang = $newlang;
}
}
if (my $newtheme = ($cookie =~ /(?:^|\s)PVEThemeCookie=([^;]*)/)[0]) {
# theme names need to be kebab case, with each segment a maximum of 10 characters long
# and at most 6 segments
if ($newtheme =~ m/^[a-z]{1,10}(-[a-z]{1,10}){0,5}$/) {
$theme = $newtheme;
}
}
if (my $newtheme = ($cookie =~ /(?:^|\s)PVEThemeCookie=([^;]*)/)[0]) {
# theme names need to be kebab case, with each segment a maximum of 10 characters long
# and at most 6 segments
if ($newtheme =~ m/^[a-z]{1,10}(-[a-z]{1,10}){0,5}$/) {
$theme = $newtheme;
}
}
my $ticket = PVE::APIServer::Formatter::extract_auth_value($cookie, $server->{cookie_name});
if (($username = PVE::AccessControl::verify_ticket($ticket, 1))) {
$token = PVE::AccessControl::assemble_csrf_prevention_token($username);
}
my $ticket = PVE::APIServer::Formatter::extract_auth_value($cookie, $server->{cookie_name});
if (($username = PVE::AccessControl::verify_ticket($ticket, 1))) {
$token = PVE::AccessControl::assemble_csrf_prevention_token($username);
}
}
my $consent_text;
eval {
my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
$consent_text = $dc_conf->{'consent-text'};
my $dc_conf = PVE::Cluster::cfs_read_file('datacenter.cfg');
$consent_text = $dc_conf->{'consent-text'};
if (!$lang) {
$lang = $dc_conf->{language} // 'en';
}
if (!$lang) {
$lang = $dc_conf->{language} // 'en';
}
};
warn "$@\n" if $@;
my $mobile = (is_phone($r->header('User-Agent')) && (!defined($args->{mobile}) || $args->{mobile})) || $args->{mobile};
my $mobile =
(is_phone($r->header('User-Agent')) && (!defined($args->{mobile}) || $args->{mobile}))
|| $args->{mobile};
my $novnc = defined($args->{console}) && $args->{novnc};
my $xtermjs = defined($args->{console}) && $args->{xtermjs};
@ -245,36 +250,36 @@ sub get_index {
my $debug = $server->{debug};
if (exists $args->{debug}) {
$debug = !defined($args->{debug}) || $args->{debug};
$debug = !defined($args->{debug}) || $args->{debug};
}
my $vars = {
lang => $lang,
langfile => $langfile,
username => $username || '',
token => $token,
console => $args->{console},
nodename => $nodename,
debug => $debug,
version => "$version",
wtversion => $wtversion,
theme => $theme,
consenttext => $consent_text
lang => $lang,
langfile => $langfile,
username => $username || '',
token => $token,
console => $args->{console},
nodename => $nodename,
debug => $debug,
version => "$version",
wtversion => $wtversion,
theme => $theme,
consenttext => $consent_text,
};
# by default, load the normal index
my $dir = $basedirs->{manager};
if ($novnc) {
$dir = $basedirs->{novnc};
$dir = $basedirs->{novnc};
} elsif ($xtermjs) {
$dir = $basedirs->{xtermjs};
$dir = $basedirs->{xtermjs};
} elsif ($mobile) {
$dir = "$basedirs->{manager}/touch";
$dir = "$basedirs->{manager}/touch";
}
my $page = '';
my $template = Template->new({ABSOLUTE => 1});
my $template = Template->new({ ABSOLUTE => 1 });
$template->process("$dir/index.html.tpl", $vars, \$page) || die $template->error(), "\n";

View file

@ -21,28 +21,28 @@ my @JOB_TYPES = qw(replication jobs);
my sub running_job_pids : prototype($) {
my ($self) = @_;
my $pids = [ map { keys $_->%* } values $self->{jobs}->%* ];
my $pids = [map { keys $_->%* } values $self->{jobs}->%*];
return scalar($pids->@*) ? $pids : undef;
}
my sub finish_jobs : prototype($) {
my ($self) = @_;
for my $type (@JOB_TYPES) {
for my $cpid (keys $self->{jobs}->{$type}->%*) {
if (my $waitpid = waitpid($cpid, WNOHANG)) {
delete $self->{jobs}->{$type}->{$cpid} if $waitpid == $cpid || $waitpid == -1;
}
}
for my $cpid (keys $self->{jobs}->{$type}->%*) {
if (my $waitpid = waitpid($cpid, WNOHANG)) {
delete $self->{jobs}->{$type}->{$cpid} if $waitpid == $cpid || $waitpid == -1;
}
}
}
};
}
sub hup {
my ($self) = @_;
my $old_workers = "";
for my $type (@JOB_TYPES) {
my $worker = $self->{jobs}->{$type} // next;
$old_workers .= "$type:$_;" for keys $worker->%*;
my $worker = $self->{jobs}->{$type} // next;
$old_workers .= "$type:$_;" for keys $worker->%*;
}
$ENV{"PVE_DAEMON_WORKER_PIDS"} = $old_workers;
$self->{got_hup_signal} = 1;
@ -56,107 +56,111 @@ sub run {
# modelled after PVE::Daemons logic, but with type added to PID
if (my $wpids = $ENV{PVE_DAEMON_WORKER_PIDS}) {
print STDERR "got workers from previous daemon run: $wpids\n"; # FIXME: only log on debug?
for my $pid (split(';', $wpids)) {
if ($pid =~ m/^(\w+):(\d+)$/) { # check & untaint
$self->{jobs}->{$1}->{$2} = 1;
} else {
warn "could not parse previous pid entry '$pid', ignoring\n";
}
}
print STDERR "got workers from previous daemon run: $wpids\n"; # FIXME: only log on debug?
for my $pid (split(';', $wpids)) {
if ($pid =~ m/^(\w+):(\d+)$/) { # check & untaint
$self->{jobs}->{$1}->{$2} = 1;
} else {
warn "could not parse previous pid entry '$pid', ignoring\n";
}
}
}
my $old_sig_chld = $SIG{CHLD};
local $SIG{CHLD} = sub {
local ($@, $!, $?); # do not overwrite error vars
finish_jobs($self);
$old_sig_chld->(@_) if $old_sig_chld;
local ($@, $!, $?); # do not overwrite error vars
finish_jobs($self);
$old_sig_chld->(@_) if $old_sig_chld;
};
my $fork = sub {
my ($type, $sub) = @_;
my ($type, $sub) = @_;
# don't fork again if the previous iteration still runs
# FIXME: some job types may handle this better themself or just not care - make configurable
return if scalar(keys $self->{jobs}->{$type}->%*);
# don't fork again if the previous iteration still runs
# FIXME: some job types may handle this better themself or just not care - make configurable
return if scalar(keys $self->{jobs}->{$type}->%*);
my $child = fork();
if (!defined($child)) {
die "fork failed: $!\n";
} elsif ($child == 0) {
$self->after_fork_cleanup();
eval {
$sub->();
};
if (my $err = $@) {
syslog('err', "$type: $err");
}
POSIX::_exit(0);
}
my $child = fork();
if (!defined($child)) {
die "fork failed: $!\n";
} elsif ($child == 0) {
$self->after_fork_cleanup();
eval { $sub->(); };
if (my $err = $@) {
syslog('err', "$type: $err");
}
POSIX::_exit(0);
}
$jobs->{$type}->{$child} = 1;
$jobs->{$type}->{$child} = 1;
};
my $first_run = 1;
my $run_jobs = sub {
# TODO: actually integrate replication in PVE::Jobs and do not always fork here, we could
# do the state lookup and check if there's new work scheduled before doing so, e.g., by
# extending the PVE::Jobs interfacae e.g.;
# my $scheduled_jobs = PVE::Jobs::get_pending() or return;
# forked { PVE::Jobs::run_jobs($scheduled_jobs) }
# TODO: actually integrate replication in PVE::Jobs and do not always fork here, we could
# do the state lookup and check if there's new work scheduled before doing so, e.g., by
# extending the PVE::Jobs interfacae e.g.;
# my $scheduled_jobs = PVE::Jobs::get_pending() or return;
# forked { PVE::Jobs::run_jobs($scheduled_jobs) }
$fork->('replication', sub {
PVE::API2::Replication::run_jobs(undef, sub {}, 0, 1);
});
$fork->(
'replication',
sub {
PVE::API2::Replication::run_jobs(undef, sub { }, 0, 1);
},
);
$fork->('jobs', sub {
PVE::Jobs::run_jobs($first_run);
});
$fork->(
'jobs',
sub {
PVE::Jobs::run_jobs($first_run);
},
);
$first_run = 0;
$first_run = 0;
};
PVE::Jobs::setup_dirs();
for (my $count = 1000;;$count++) {
return if $self->{got_hup_signal}; # keep workers running, PVE::Daemon re-execs us on return
last if $self->{shutdown_request}; # exit main-run loop for shutdown
for (my $count = 1000;; $count++) {
return if $self->{got_hup_signal}; # keep workers running, PVE::Daemon re-execs us on return
last if $self->{shutdown_request}; # exit main-run loop for shutdown
$run_jobs->();
$run_jobs->();
my $sleep_time = 60;
if ($count >= 1000) {
# Job schedule has minute precision, so try running near the minute boundary.
my ($current_seconds) = localtime;
$sleep_time = (60 - $current_seconds) if (60 - $current_seconds >= 5);
$count = 0;
}
my $sleep_time = 60;
if ($count >= 1000) {
# Job schedule has minute precision, so try running near the minute boundary.
my ($current_seconds) = localtime;
$sleep_time = (60 - $current_seconds) if (60 - $current_seconds >= 5);
$count = 0;
}
my $slept = 0; # SIGCHLD interrupts sleep, so we need to keep track
while ($slept < $sleep_time) {
last if $self->{shutdown_request} || $self->{got_hup_signal};
$slept += sleep($sleep_time - $slept);
# TODO: check if there's new work to do, e.g., if a job finished
# that had a longer runtime than run period
}
my $slept = 0; # SIGCHLD interrupts sleep, so we need to keep track
while ($slept < $sleep_time) {
last if $self->{shutdown_request} || $self->{got_hup_signal};
$slept += sleep($sleep_time - $slept);
# TODO: check if there's new work to do, e.g., if a job finished
# that had a longer runtime than run period
}
}
# NOTE: we only get here on shutdown_request, so we already sent a TERM to all job-types
my $timeout = 0;
while(my $pids = running_job_pids($self)) {
kill 'TERM', $pids->@*; # send TERM to all workers at once, possible thundering herd - FIXME?
while (my $pids = running_job_pids($self)) {
kill 'TERM', $pids->@*; # send TERM to all workers at once, possible thundering herd - FIXME?
finish_jobs($self);
finish_jobs($self);
# some jobs have a lock timeout of 60s, wait a bit more for graceful termination
last if $timeout > 75;
$timeout += sleep(3);
# some jobs have a lock timeout of 60s, wait a bit more for graceful termination
last if $timeout > 75;
$timeout += sleep(3);
}
if (my $pids = running_job_pids($self)) {
syslog('warn', "unresponsive job-worker, killing now: " . join(', ', $pids->@*));
kill 'KILL', $pids->@*;
syslog('warn', "unresponsive job-worker, killing now: " . join(', ', $pids->@*));
kill 'KILL', $pids->@*;
}
}
@ -166,7 +170,7 @@ sub shutdown {
syslog('info', 'got shutdown request, signal running jobs to stop');
for my $jobs (values $self->{jobs}->%*) {
kill 'TERM', keys $jobs->%*;
kill 'TERM', keys $jobs->%*;
}
$self->{shutdown_request} = 1;
}
@ -177,10 +181,10 @@ $daemon->register_restart_command(1);
$daemon->register_status_command();
our $cmddef = {
start => [ __PACKAGE__, 'start', []],
stop => [ __PACKAGE__, 'stop', []],
restart => [ __PACKAGE__, 'restart', []],
status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
start => [__PACKAGE__, 'start', []],
stop => [__PACKAGE__, 'stop', []],
restart => [__PACKAGE__, 'restart', []],
status => [__PACKAGE__, 'status', [], undef, sub { print shift . "\n"; }],
};
1;

View file

@ -64,7 +64,7 @@ sub init {
sub shutdown {
my ($self) = @_;
syslog('info' , "server closing");
syslog('info', "server closing");
# wait for children
1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
@ -90,32 +90,37 @@ sub update_supported_cpuflags {
return if $cached_kvm_version && $cached_kvm_version eq $kvm_version;
if ($next_flag_update_time && $next_flag_update_time > time()) {
return;
return;
}
$next_flag_update_time = 0;
my $supported_cpuflags = eval { PVE::QemuServer::query_supported_cpu_flags() };
warn $@ if $@;
if (!$supported_cpuflags ||
(!$supported_cpuflags->{tcg} && !$supported_cpuflags->{kvm})) {
# something went wrong, clear broadcast flags and set try-again delay
warn "CPU flag detection failed, will try again after delay\n";
$next_flag_update_time = time() + $failed_flag_update_delay_sec;
if (
!$supported_cpuflags
|| (!$supported_cpuflags->{tcg} && !$supported_cpuflags->{kvm})
) {
# something went wrong, clear broadcast flags and set try-again delay
warn "CPU flag detection failed, will try again after delay\n";
$next_flag_update_time = time() + $failed_flag_update_delay_sec;
$supported_cpuflags = {};
$supported_cpuflags = {};
} else {
# only set cached version if there's actually something to broadcast
$cached_kvm_version = $kvm_version;
# only set cached version if there's actually something to broadcast
$cached_kvm_version = $kvm_version;
}
for my $accel ("tcg", "kvm") {
if ($supported_cpuflags->{$accel}) {
PVE::Cluster::broadcast_node_kv("cpuflags-$accel", join(' ', @{$supported_cpuflags->{$accel}}));
} else {
# clear potentially invalid data
PVE::Cluster::broadcast_node_kv("cpuflags-$accel", '');
}
if ($supported_cpuflags->{$accel}) {
PVE::Cluster::broadcast_node_kv(
"cpuflags-$accel",
join(' ', @{ $supported_cpuflags->{$accel} }),
);
} else {
# clear potentially invalid data
PVE::Cluster::broadcast_node_kv("cpuflags-$accel", '');
}
}
}
@ -135,16 +140,18 @@ my sub broadcast_static_node_info {
$old = eval { decode_json($old->{$nodename}) } if defined($old->{$nodename});
if (
!defined($old->{cpus}) || $old->{cpus} != $cpus
|| !defined($old->{memory}) || $old->{memory} != $memory
|| ($old->{'cgroup-mode'} // -1) != ($cgroup_mode // -1)
!defined($old->{cpus})
|| $old->{cpus} != $cpus
|| !defined($old->{memory})
|| $old->{memory} != $memory
|| ($old->{'cgroup-mode'} // -1) != ($cgroup_mode // -1)
) {
my $info = {
cpus => $cpus,
memory => $memory,
};
$info->{'cgroup-mode'} = $cgroup_mode if defined($cgroup_mode);
PVE::Cluster::broadcast_node_kv('static-info', encode_json($info));
my $info = {
cpus => $cpus,
memory => $memory,
};
$info->{'cgroup-mode'} = $cgroup_mode if defined($cgroup_mode);
PVE::Cluster::broadcast_node_kv('static-info', encode_json($info));
}
}
@ -167,32 +174,45 @@ sub update_node_status {
# traffic from/to physical interface cards
my ($netin, $netout) = (0, 0);
for my $dev (grep { /^$PVE::Network::PHYSICAL_NIC_RE$/ } keys %$netdev) {
$netin += $netdev->{$dev}->{receive};
$netout += $netdev->{$dev}->{transmit};
$netin += $netdev->{$dev}->{receive};
$netout += $netdev->{$dev}->{transmit};
}
my $meminfo = PVE::ProcFSTools::read_meminfo();
my $dinfo = df('/', 1); # output is bytes
my $dinfo = df('/', 1); # output is bytes
# everything not free is considered to be used
my $dused = $dinfo->{blocks} - $dinfo->{bfree};
my $ctime = time();
my $data = $generate_rrd_string->(
[$uptime, $sublevel, $ctime, $avg1, $maxcpu, $stat->{cpu}, $stat->{wait},
$meminfo->{memtotal}, $meminfo->{memused},
$meminfo->{swaptotal}, $meminfo->{swapused},
$dinfo->{blocks}, $dused, $netin, $netout]
[
$uptime,
$sublevel,
$ctime,
$avg1,
$maxcpu,
$stat->{cpu},
$stat->{wait},
$meminfo->{memtotal},
$meminfo->{memused},
$meminfo->{swaptotal},
$meminfo->{swapused},
$dinfo->{blocks},
$dused,
$netin,
$netout,
],
);
PVE::Cluster::broadcast_rrd("pve2-node/$nodename", $data);
my $node_metric = {
uptime => $uptime,
cpustat => $stat,
memory => $meminfo,
blockstat => $dinfo,
nics => $netdev,
uptime => $uptime,
cpustat => $stat,
memory => $meminfo,
blockstat => $dinfo,
nics => $netdev,
};
$node_metric->{cpustat}->@{qw(avg1 avg5 avg15)} = ($avg1, $avg5, $avg15);
$node_metric->{cpustat}->{cpus} = $maxcpu;
@ -207,7 +227,7 @@ sub update_node_status {
}
sub auto_balloning {
my ($vmstatus) = @_;
my ($vmstatus) = @_;
my $log = sub { $opt_debug and printf @_ };
@ -221,19 +241,20 @@ sub auto_balloning {
my $target = int($config->{'ballooning-target'} // 80);
# goal is the change amount required to achieve that
my $goal = int($hostmeminfo->{memtotal} * $target / 100 - $hostmeminfo->{memused});
$log->("target: $target%% host goal: $goal free: $hostfreemem total: $hostmeminfo->{memtotal}\n");
$log->(
"target: $target%% host goal: $goal free: $hostfreemem total: $hostmeminfo->{memtotal}\n");
my $maxchange = 100*1024*1024;
my $maxchange = 100 * 1024 * 1024;
my $res = PVE::AutoBalloon::compute_alg1($vmstatus, $goal, $maxchange);
for my $vmid (sort keys %$res) {
my $target = int($res->{$vmid});
my $current = int($vmstatus->{$vmid}->{balloon});
next if $target == $current; # no need to change
my $target = int($res->{$vmid});
my $current = int($vmstatus->{$vmid}->{balloon});
next if $target == $current; # no need to change
$log->("BALLOON $vmid to $target (%d)\n", $target - $current);
eval { PVE::QemuServer::Monitor::mon_cmd($vmid, "balloon", value => int($target)) };
warn $@ if $@;
$log->("BALLOON $vmid to $target (%d)\n", $target - $current);
eval { PVE::QemuServer::Monitor::mon_cmd($vmid, "balloon", value => int($target)) };
warn $@ if $@;
}
}
@ -248,23 +269,50 @@ sub update_qemu_status {
my $transactions = PVE::ExtMetric::transactions_start($status_cfg);
foreach my $vmid (keys %$vmstatus) {
my $d = $vmstatus->{$vmid};
my $data;
my $status = $d->{qmpstatus} || $d->{status} || 'stopped';
my $template = $d->{template} ? $d->{template} : "0";
if ($d->{pid}) { # running
$data = $generate_rrd_string->(
[$d->{uptime}, $d->{name}, $status, $template, $ctime, $d->{cpus}, $d->{cpu},
$d->{maxmem}, $d->{mem}, $d->{maxdisk}, $d->{disk},
$d->{netin}, $d->{netout}, $d->{diskread}, $d->{diskwrite}]);
} else {
$data = $generate_rrd_string->(
[0, $d->{name}, $status, $template, $ctime, $d->{cpus}, undef,
$d->{maxmem}, undef, $d->{maxdisk}, $d->{disk}, undef, undef, undef, undef]);
}
PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
my $d = $vmstatus->{$vmid};
my $data;
my $status = $d->{qmpstatus} || $d->{status} || 'stopped';
my $template = $d->{template} ? $d->{template} : "0";
if ($d->{pid}) { # running
$data = $generate_rrd_string->([
$d->{uptime},
$d->{name},
$status,
$template,
$ctime,
$d->{cpus},
$d->{cpu},
$d->{maxmem},
$d->{mem},
$d->{maxdisk},
$d->{disk},
$d->{netin},
$d->{netout},
$d->{diskread},
$d->{diskwrite},
]);
} else {
$data = $generate_rrd_string->([
0,
$d->{name},
$status,
$template,
$ctime,
$d->{cpus},
undef,
$d->{maxmem},
undef,
$d->{maxdisk},
$d->{disk},
undef,
undef,
undef,
undef,
]);
}
PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
PVE::ExtMetric::update_all($transactions, 'qemu', $vmid, $d, $ctime, $nodename);
PVE::ExtMetric::update_all($transactions, 'qemu', $vmid, $d, $ctime, $nodename);
}
PVE::ExtMetric::transactions_finish($transactions);
@ -278,28 +326,29 @@ sub remove_stale_lxc_consoles {
my $pidhash = PVE::LXC::find_lxc_console_pids();
foreach my $vmid (keys %$pidhash) {
next if defined($vmstatus->{$vmid});
syslog('info', "remove stale lxc-console for CT $vmid");
foreach my $pid (@{$pidhash->{$vmid}}) {
kill(9, $pid);
}
next if defined($vmstatus->{$vmid});
syslog('info', "remove stale lxc-console for CT $vmid");
foreach my $pid (@{ $pidhash->{$vmid} }) {
kill(9, $pid);
}
}
}
my $rebalance_error_count = {};
my $NO_REBALANCE;
sub rebalance_lxc_containers {
# Make sure we can find the cpuset controller path:
return if $NO_REBALANCE;
my $cpuset_base = eval { PVE::CGroup::cpuset_controller_path() };
if (my $err = $@) {
syslog('info', "could not get cpuset controller path: $err");
syslog('info', "could not get cpuset controller path: $err");
}
if (!defined($cpuset_base)) {
$NO_REBALANCE = 1;
return;
$NO_REBALANCE = 1;
return;
}
# Figure out the cpu count & highest ID
@ -308,7 +357,7 @@ sub rebalance_lxc_containers {
my $cpucount = scalar(@allowed_cpus);
my $max_cpuid = $allowed_cpus[-1];
my @cpu_ctcount = (0) x ($max_cpuid+1);
my @cpu_ctcount = (0) x ($max_cpuid + 1);
my @balanced_cts;
# A mapping { vmid => cgroup_payload_path } for containers where namespace
@ -316,135 +365,132 @@ sub rebalance_lxc_containers {
my $ctinfo = {};
my $modify_cpuset = sub {
my ($vmid, $cpuset, $newset) = @_;
my ($vmid, $cpuset, $newset) = @_;
if (!$rebalance_error_count->{$vmid}) {
syslog('info', "modified cpu set for lxc/$vmid: " . $newset->short_string());
}
if (!$rebalance_error_count->{$vmid}) {
syslog('info', "modified cpu set for lxc/$vmid: " . $newset->short_string());
}
eval {
my $cgbase = $ctinfo->{$vmid};
eval {
my $cgbase = $ctinfo->{$vmid};
if (defined($cgbase)) {
# allow all, so that we can set new cpuset in /ns
$all_cpus->write_to_path($cgbase);
eval {
$newset->write_to_path("$cgbase/ns");
};
if (my $err = $@) {
warn $err if !$rebalance_error_count->{$vmid}++;
# restore original
$cpuset->write_to_path($cgbase);
} else {
# also apply to container root cgroup
$newset->write_to_path($cgbase);
$rebalance_error_count->{$vmid} = 0;
}
} else {
# old style container
$newset->write_to_path($cgbase);
$rebalance_error_count->{$vmid} = 0;
}
};
if (my $err = $@) {
warn $err if !$rebalance_error_count->{$vmid}++;
}
if (defined($cgbase)) {
# allow all, so that we can set new cpuset in /ns
$all_cpus->write_to_path($cgbase);
eval { $newset->write_to_path("$cgbase/ns"); };
if (my $err = $@) {
warn $err if !$rebalance_error_count->{$vmid}++;
# restore original
$cpuset->write_to_path($cgbase);
} else {
# also apply to container root cgroup
$newset->write_to_path($cgbase);
$rebalance_error_count->{$vmid} = 0;
}
} else {
# old style container
$newset->write_to_path($cgbase);
$rebalance_error_count->{$vmid} = 0;
}
};
if (my $err = $@) {
warn $err if !$rebalance_error_count->{$vmid}++;
}
};
my $ctlist = PVE::LXC::config_list();
foreach my $vmid (sort keys %$ctlist) {
my $cgpath = "$cpuset_base/lxc/$vmid";
if (-d "$cgpath/ns") {
$ctinfo->{$vmid} = $cgpath;
} else {
next; # old style container
}
my $cgpath = "$cpuset_base/lxc/$vmid";
if (-d "$cgpath/ns") {
$ctinfo->{$vmid} = $cgpath;
} else {
next; # old style container
}
my ($conf, $cpuset) = eval {(
PVE::LXC::Config->load_config($vmid),
PVE::CpuSet->new_from_path($cgpath),
)};
if (my $err = $@) {
warn $err;
next;
}
my ($conf, $cpuset) =
eval { (PVE::LXC::Config->load_config($vmid), PVE::CpuSet->new_from_path($cgpath)) };
if (my $err = $@) {
warn $err;
next;
}
my @cpuset_members = $cpuset->members();
my @cpuset_members = $cpuset->members();
if (!PVE::LXC::Config->has_lxc_entry($conf, 'lxc.cgroup.cpuset.cpus')
&& !PVE::LXC::Config->has_lxc_entry($conf, 'lxc.cgroup2.cpuset.cpus')
) {
my $cores = $conf->{cores} || $cpucount;
$cores = $cpucount if $cores > $cpucount;
if (
!PVE::LXC::Config->has_lxc_entry($conf, 'lxc.cgroup.cpuset.cpus')
&& !PVE::LXC::Config->has_lxc_entry($conf, 'lxc.cgroup2.cpuset.cpus')
) {
my $cores = $conf->{cores} || $cpucount;
$cores = $cpucount if $cores > $cpucount;
# see if the number of cores was hot-reduced or hasn't been enacted at all yet
my $newset = PVE::CpuSet->new();
if ($cores < scalar(@cpuset_members)) {
for (my $i = 0; $i < $cores; $i++) {
$newset->insert($cpuset_members[$i]);
}
} elsif ($cores > scalar(@cpuset_members)) {
my $count = $newset->insert(@cpuset_members);
foreach my $cpu (@allowed_cpus) {
$count += $newset->insert($cpu);
last if $count >= $cores;
}
} else {
$newset->insert(@cpuset_members);
}
# see if the number of cores was hot-reduced or hasn't been enacted at all yet
my $newset = PVE::CpuSet->new();
if ($cores < scalar(@cpuset_members)) {
for (my $i = 0; $i < $cores; $i++) {
$newset->insert($cpuset_members[$i]);
}
} elsif ($cores > scalar(@cpuset_members)) {
my $count = $newset->insert(@cpuset_members);
foreach my $cpu (@allowed_cpus) {
$count += $newset->insert($cpu);
last if $count >= $cores;
}
} else {
$newset->insert(@cpuset_members);
}
# Apply hot-plugged changes if any:
if (!$newset->is_equal($cpuset)) {
@cpuset_members = $newset->members();
$modify_cpuset->($vmid, $cpuset, $newset);
}
# Apply hot-plugged changes if any:
if (!$newset->is_equal($cpuset)) {
@cpuset_members = $newset->members();
$modify_cpuset->($vmid, $cpuset, $newset);
}
# Note: no need to rebalance if we already use all cores
push @balanced_cts, [$vmid, $cores, $newset]
if defined($conf->{cores}) && ($cores != $cpucount);
}
# Note: no need to rebalance if we already use all cores
push @balanced_cts, [$vmid, $cores, $newset]
if defined($conf->{cores}) && ($cores != $cpucount);
}
foreach my $cpu (@cpuset_members) {
$cpu_ctcount[$cpu]++ if $cpu <= $max_cpuid;
}
foreach my $cpu (@cpuset_members) {
$cpu_ctcount[$cpu]++ if $cpu <= $max_cpuid;
}
}
my $find_best_cpu = sub {
my ($cpulist, $cpu) = @_;
my ($cpulist, $cpu) = @_;
my $cur_cost = $cpu_ctcount[$cpu];
my $cur_cpu = $cpu;
my $cur_cost = $cpu_ctcount[$cpu];
my $cur_cpu = $cpu;
foreach my $candidate (@$cpulist) {
my $cost = $cpu_ctcount[$candidate];
if ($cost < ($cur_cost - 1)) {
$cur_cost = $cost;
$cur_cpu = $candidate;
}
}
foreach my $candidate (@$cpulist) {
my $cost = $cpu_ctcount[$candidate];
if ($cost < ($cur_cost - 1)) {
$cur_cost = $cost;
$cur_cpu = $candidate;
}
}
return $cur_cpu;
return $cur_cpu;
};
foreach my $bct (@balanced_cts) {
my ($vmid, $cores, $cpuset) = @$bct;
my ($vmid, $cores, $cpuset) = @$bct;
my $rest = [ grep { !$cpuset->has($_) } @allowed_cpus ];
my $rest = [grep { !$cpuset->has($_) } @allowed_cpus];
my $newset = PVE::CpuSet->new();
for my $cpu ($cpuset->members()) {
my $best = $find_best_cpu->($rest, $cpu);
if ($best != $cpu) {
$cpu_ctcount[$best]++;
$cpu_ctcount[$cpu]--;
}
$newset->insert($best);
}
my $newset = PVE::CpuSet->new();
for my $cpu ($cpuset->members()) {
my $best = $find_best_cpu->($rest, $cpu);
if ($best != $cpu) {
$cpu_ctcount[$best]++;
$cpu_ctcount[$cpu]--;
}
$newset->insert($best);
}
if (!$newset->is_equal($cpuset)) {
$modify_cpuset->($vmid, $cpuset, $newset);
}
if (!$newset->is_equal($cpuset)) {
$modify_cpuset->($vmid, $cpuset, $newset);
}
}
}
@ -457,25 +503,49 @@ sub update_lxc_status {
my $transactions = PVE::ExtMetric::transactions_start($status_cfg);
foreach my $vmid (keys %$vmstatus) {
my $d = $vmstatus->{$vmid};
my $template = $d->{template} ? $d->{template} : "0";
my $data;
if ($d->{status} eq 'running') { # running
$data = $generate_rrd_string->(
[$d->{uptime}, $d->{name}, $d->{status}, $template,
$ctime, $d->{cpus}, $d->{cpu},
$d->{maxmem}, $d->{mem},
$d->{maxdisk}, $d->{disk},
$d->{netin}, $d->{netout},
$d->{diskread}, $d->{diskwrite}]);
} else {
$data = $generate_rrd_string->(
[0, $d->{name}, $d->{status}, $template, $ctime, $d->{cpus}, undef,
$d->{maxmem}, undef, $d->{maxdisk}, $d->{disk}, undef, undef, undef, undef]);
}
PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
my $d = $vmstatus->{$vmid};
my $template = $d->{template} ? $d->{template} : "0";
my $data;
if ($d->{status} eq 'running') { # running
$data = $generate_rrd_string->([
$d->{uptime},
$d->{name},
$d->{status},
$template,
$ctime,
$d->{cpus},
$d->{cpu},
$d->{maxmem},
$d->{mem},
$d->{maxdisk},
$d->{disk},
$d->{netin},
$d->{netout},
$d->{diskread},
$d->{diskwrite},
]);
} else {
$data = $generate_rrd_string->([
0,
$d->{name},
$d->{status},
$template,
$ctime,
$d->{cpus},
undef,
$d->{maxmem},
undef,
$d->{maxdisk},
$d->{disk},
undef,
undef,
undef,
undef,
]);
}
PVE::Cluster::broadcast_rrd("pve2.3-vm/$vmid", $data);
PVE::ExtMetric::update_all($transactions, 'lxc', $vmid, $d, $ctime, $nodename);
PVE::ExtMetric::update_all($transactions, 'lxc', $vmid, $d, $ctime, $nodename);
}
PVE::ExtMetric::transactions_finish($transactions);
@ -492,15 +562,15 @@ sub update_storage_status {
my $transactions = PVE::ExtMetric::transactions_start($status_cfg);
foreach my $storeid (keys %$info) {
my $d = $info->{$storeid};
next if !$d->{active};
my $d = $info->{$storeid};
next if !$d->{active};
my $data = $generate_rrd_string->([$ctime, $d->{total}, $d->{used}]);
my $data = $generate_rrd_string->([$ctime, $d->{total}, $d->{used}]);
my $key = "pve2-storage/${nodename}/$storeid";
PVE::Cluster::broadcast_rrd($key, $data);
my $key = "pve2-storage/${nodename}/$storeid";
PVE::Cluster::broadcast_rrd($key, $data);
PVE::ExtMetric::update_all($transactions, 'storage', $nodename, $storeid, $d, $ctime);
PVE::ExtMetric::update_all($transactions, 'storage', $nodename, $storeid, $d, $ctime);
}
PVE::ExtMetric::transactions_finish($transactions);
@ -521,22 +591,21 @@ sub update_ceph_metadata {
sub update_sdn_status {
if($have_sdn) {
my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
if ($have_sdn) {
my ($transport_status, $vnet_status) = PVE::Network::SDN::status();
my $status = $transport_status ? encode_json($transport_status) : undef;
PVE::Cluster::broadcast_node_kv("sdn", $status);
my $status = $transport_status ? encode_json($transport_status) : undef;
PVE::Cluster::broadcast_node_kv("sdn", $status);
}
}
my $broadcast_version_info_done = 0;
my sub broadcast_version_info : prototype() {
if (!$broadcast_version_info_done) {
PVE::Cluster::broadcast_node_kv(
'version-info',
encode_json(PVE::pvecfg::version_info()),
);
$broadcast_version_info_done = 1;
PVE::Cluster::broadcast_node_kv(
'version-info', encode_json(PVE::pvecfg::version_info()),
);
$broadcast_version_info_done = 1;
}
}
@ -548,77 +617,55 @@ sub update_status {
my $pull_txn = PVE::PullMetric::transaction_start();
eval {
my $tlist = $rpcenv->active_workers();
PVE::Cluster::broadcast_tasklist($tlist);
my $tlist = $rpcenv->active_workers();
PVE::Cluster::broadcast_tasklist($tlist);
};
my $err = $@;
syslog('err', $err) if $err;
my $status_cfg = PVE::Cluster::cfs_read_file('status.cfg');
eval {
update_node_status($status_cfg, $pull_txn);
};
eval { update_node_status($status_cfg, $pull_txn); };
$err = $@;
syslog('err', "node status update error: $err") if $err;
eval {
update_qemu_status($status_cfg, $pull_txn);
};
eval { update_qemu_status($status_cfg, $pull_txn); };
$err = $@;
syslog('err', "qemu status update error: $err") if $err;
eval {
update_lxc_status($status_cfg, $pull_txn);
};
eval { update_lxc_status($status_cfg, $pull_txn); };
$err = $@;
syslog('err', "lxc status update error: $err") if $err;
eval {
rebalance_lxc_containers();
};
eval { rebalance_lxc_containers(); };
$err = $@;
syslog('err', "lxc cpuset rebalance error: $err") if $err;
eval {
update_storage_status($status_cfg, $pull_txn);
};
eval { update_storage_status($status_cfg, $pull_txn); };
$err = $@;
syslog('err', "storage status update error: $err") if $err;
eval {
remove_stale_lxc_consoles();
};
eval { remove_stale_lxc_consoles(); };
$err = $@;
syslog('err', "lxc console cleanup error: $err") if $err;
eval {
rotate_authkeys();
};
eval { rotate_authkeys(); };
$err = $@;
syslog('err', "authkey rotation error: $err") if $err;
eval {
update_ceph_metadata();
};
eval { update_ceph_metadata(); };
$err = $@;
syslog('err', "ceph metadata update error: $err") if $err;
eval {
update_sdn_status();
};
eval { update_sdn_status(); };
$err = $@;
syslog('err', "sdn status update error: $err") if $err;
eval {
broadcast_version_info();
};
eval { broadcast_version_info(); };
$err = $@;
syslog('err', "version info update error: $err") if $err;
eval {
PVE::PullMetric::transaction_finish($pull_txn);
};
eval { PVE::PullMetric::transaction_finish($pull_txn); };
$err = $@;
syslog('err', "could not populate metric data cache: $err") if $err;
}
@ -627,7 +674,7 @@ my $next_update = 0;
# do not update directly after startup, because install scripts
# have a problem with that
my $cycle = 0;
my $cycle = 0;
my $updatetime = 10;
my $initial_memory_usage;
@ -637,50 +684,59 @@ sub run {
for (;;) { # forever
$next_update = time() + $updatetime;
$next_update = time() + $updatetime;
if ($cycle) {
my ($ccsec, $cusec) = gettimeofday ();
eval {
# syslog('info', "start status update");
PVE::Cluster::cfs_update();
update_status();
};
my $err = $@;
if ($cycle) {
my ($ccsec, $cusec) = gettimeofday();
eval {
# syslog('info', "start status update");
PVE::Cluster::cfs_update();
update_status();
};
my $err = $@;
if ($err) {
syslog('err', "status update error: $err");
}
if ($err) {
syslog('err', "status update error: $err");
}
my ($ccsec_end, $cusec_end) = gettimeofday ();
my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000;
my ($ccsec_end, $cusec_end) = gettimeofday();
my $cptime = ($ccsec_end - $ccsec) + ($cusec_end - $cusec) / 1000000;
syslog('info', sprintf("status update time (%.3f seconds)", $cptime))
if ($cptime > 5);
}
syslog('info', sprintf("status update time (%.3f seconds)", $cptime))
if ($cptime > 5);
}
$cycle++;
$cycle++;
my $mem = PVE::ProcFSTools::read_memory_usage();
my $resident_kb = $mem->{resident} / 1024;
my $mem = PVE::ProcFSTools::read_memory_usage();
my $resident_kb = $mem->{resident} / 1024;
if (!defined($initial_memory_usage) || ($cycle < 10)) {
$initial_memory_usage = $resident_kb;
} else {
my $diff = $resident_kb - $initial_memory_usage;
if ($diff > 15 * 1024) {
syslog ('info', "restarting server after $cycle cycles to " .
"reduce memory usage (free $resident_kb ($diff) KB)");
$self->restart_daemon();
}
}
if (!defined($initial_memory_usage) || ($cycle < 10)) {
$initial_memory_usage = $resident_kb;
} else {
my $diff = $resident_kb - $initial_memory_usage;
if ($diff > 15 * 1024) {
syslog(
'info',
"restarting server after $cycle cycles to "
. "reduce memory usage (free $resident_kb ($diff) KB)",
);
$self->restart_daemon();
}
}
my $wcount = 0;
while ((time() < $next_update) &&
($wcount < $updatetime) && # protect against time wrap
!$restart_request) { $wcount++; sleep (1); };
my $wcount = 0;
while (
(time() < $next_update)
&& ($wcount < $updatetime)
&& # protect against time wrap
!$restart_request
) {
$wcount++;
sleep(1);
}
$self->restart_daemon() if $restart_request;
$self->restart_daemon() if $restart_request;
}
}
@ -690,10 +746,10 @@ $daemon->register_stop_command();
$daemon->register_status_command();
our $cmddef = {
start => [ __PACKAGE__, 'start', []],
restart => [ __PACKAGE__, 'restart', []],
stop => [ __PACKAGE__, 'stop', []],
status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
start => [__PACKAGE__, 'start', []],
restart => [__PACKAGE__, 'restart', []],
stop => [__PACKAGE__, 'stop', []],
status => [__PACKAGE__, 'status', [], undef, sub { print shift . "\n"; }],
};
1;

View file

@ -1,7 +1,7 @@
package PVE::Service::spiceproxy;
# Note: In theory, all this can be done by 'pveproxy' daemon. But some
# API call still have blocking code, so we use a separate daemon to avoid
# Note: In theory, all this can be done by 'pveproxy' daemon. But some
# API call still have blocking code, so we use a separate daemon to avoid
# that the console gets blocked.
use strict;
@ -18,15 +18,15 @@ my $cmdline = [$0, @ARGV];
my %daemon_options = (
max_workers => 1, # todo: do we need more?
restart_on_error => 5,
restart_on_error => 5,
stop_wait_time => 15,
leave_children_open_on_reload => 1,
setuid => 'www-data',
setgid => 'www-data',
pidfile => '/var/run/pveproxy/spiceproxy.pid',
);
);
my $daemon = __PACKAGE__->new('spiceproxy', $cmdline, %daemon_options);
my $daemon = __PACKAGE__->new('spiceproxy', $cmdline, %daemon_options);
sub init {
my ($self) = @_;
@ -36,32 +36,32 @@ sub init {
my $accept_lock_fn = "/var/lock/spiceproxy.lck";
my $lockfh = IO::File->new(">>${accept_lock_fn}") ||
die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $lockfh = IO::File->new(">>${accept_lock_fn}")
|| die "unable to open lock file '${accept_lock_fn}' - $!\n";
my $listen_ip = $proxyconf->{LISTEN_IP};
my $socket = $self->create_reusable_socket(3128, $listen_ip);
$self->{server_config} = {
keep_alive => 0,
max_conn => 500,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
spiceproxy => 1,
trusted_env => 0,
logfile => '/var/log/pveproxy/access.log',
allow_from => $proxyconf->{ALLOW_FROM},
deny_from => $proxyconf->{DENY_FROM},
policy => $proxyconf->{POLICY},
keep_alive => 0,
max_conn => 500,
lockfile => $accept_lock_fn,
socket => $socket,
lockfh => $lockfh,
debug => $self->{debug},
spiceproxy => 1,
trusted_env => 0,
logfile => '/var/log/pveproxy/access.log',
allow_from => $proxyconf->{ALLOW_FROM},
deny_from => $proxyconf->{DENY_FROM},
policy => $proxyconf->{POLICY},
};
}
sub run {
my ($self) = @_;
my $server = PVE::HTTPServer->new(%{$self->{server_config}});
my $server = PVE::HTTPServer->new(%{ $self->{server_config} });
$server->run();
}
@ -71,10 +71,10 @@ $daemon->register_stop_command();
$daemon->register_status_command();
our $cmddef = {
start => [ __PACKAGE__, 'start', []],
restart => [ __PACKAGE__, 'restart', []],
stop => [ __PACKAGE__, 'stop', []],
status => [ __PACKAGE__, 'status', [], undef, sub { print shift . "\n";} ],
start => [__PACKAGE__, 'start', []],
restart => [__PACKAGE__, 'restart', []],
stop => [__PACKAGE__, 'stop', []],
status => [__PACKAGE__, 'status', [], undef, sub { print shift . "\n"; }],
};
1;

View file

@ -26,35 +26,36 @@ sub type {
sub properties {
return {
path => {
type => 'string', format => 'graphite-path',
description => "root graphite path (ex: proxmox.mycluster.mykey)",
},
timeout => {
type => 'integer',
description => "graphite TCP socket timeout (default=1)",
minimum => 0,
default => 1,
optional => 1
},
proto => {
type => 'string',
enum => ['udp', 'tcp'],
description => "Protocol to send graphite data. TCP or UDP (default)",
optional => 1,
},
path => {
type => 'string',
format => 'graphite-path',
description => "root graphite path (ex: proxmox.mycluster.mykey)",
},
timeout => {
type => 'integer',
description => "graphite TCP socket timeout (default=1)",
minimum => 0,
default => 1,
optional => 1,
},
proto => {
type => 'string',
enum => ['udp', 'tcp'],
description => "Protocol to send graphite data. TCP or UDP (default)",
optional => 1,
},
};
}
sub options {
return {
server => {},
port => { optional => 1 },
mtu => { optional => 1 },
proto => { optional => 1 },
timeout => { optional => 1 },
path => { optional => 1 },
disable => { optional => 1 },
server => {},
port => { optional => 1 },
mtu => { optional => 1 },
proto => { optional => 1 },
timeout => { optional => 1 },
path => { optional => 1 },
disable => { optional => 1 },
};
}
@ -88,7 +89,7 @@ sub _send_batch_size {
my ($class, $cfg) = @_;
my $proto = $cfg->{proto} || 'udp';
if ($proto eq 'tcp') {
return 56000;
return 56000;
}
return $class->SUPER::_send_batch_size($cfg);
}
@ -96,23 +97,23 @@ sub _send_batch_size {
sub _connect {
my ($class, $cfg) = @_;
my $host = $cfg->{server};
my $port = $cfg->{port} || 2003;
my $proto = $cfg->{proto} || 'udp';
my $host = $cfg->{server};
my $port = $cfg->{port} || 2003;
my $proto = $cfg->{proto} || 'udp';
my $timeout = $cfg->{timeout} // 1;
my $carbon_socket = IO::Socket::IP->new(
PeerAddr => $host,
PeerPort => $port,
Proto => $proto,
Timeout => $timeout,
PeerAddr => $host,
PeerPort => $port,
Proto => $proto,
Timeout => $timeout,
) || die "couldn't create carbon socket [$host]:$port - $@\n";
if ($proto eq 'tcp') {
# seconds and µs
my $timeout_struct = pack( 'l!l!', $timeout, 0);
setsockopt($carbon_socket, SOL_SOCKET, SO_SNDTIMEO, $timeout_struct);
setsockopt($carbon_socket, SOL_SOCKET, SO_RCVTIMEO, $timeout_struct);
# seconds and µs
my $timeout_struct = pack('l!l!', $timeout, 0);
setsockopt($carbon_socket, SOL_SOCKET, SO_SNDTIMEO, $timeout_struct);
setsockopt($carbon_socket, SOL_SOCKET, SO_RCVTIMEO, $timeout_struct);
}
return $carbon_socket;
@ -126,29 +127,29 @@ sub assemble {
# we do not want boolean/state information to export to graphite
my $key_blacklist = {
'template' => 1,
'pid' => 1,
'agent' => 1,
'serial' => 1,
'template' => 1,
'pid' => 1,
'agent' => 1,
'serial' => 1,
};
my $assemble_graphite_data;
$assemble_graphite_data = sub {
my ($metric, $path) = @_;
my ($metric, $path) = @_;
for my $key (sort keys %$metric) {
my $value = $metric->{$key};
next if !defined($value);
for my $key (sort keys %$metric) {
my $value = $metric->{$key};
next if !defined($value);
$key =~ s/\./-/g;
my $metricpath = $path . ".$key";
$key =~ s/\./-/g;
my $metricpath = $path . ".$key";
if (ref($value) eq 'HASH') {
$assemble_graphite_data->($value, $metricpath);
} elsif ($value =~ m/^[+-]?[0-9]*\.?[0-9]+$/ && !$key_blacklist->{$key}) {
$class->add_metric_data($txn, "$metricpath $value $ctime\n");
}
}
if (ref($value) eq 'HASH') {
$assemble_graphite_data->($value, $metricpath);
} elsif ($value =~ m/^[+-]?[0-9]*\.?[0-9]+$/ && !$key_blacklist->{$key}) {
$class->add_metric_data($txn, "$metricpath $value $ctime\n");
}
}
};
$assemble_graphite_data->($data, $path);
@ -156,18 +157,18 @@ sub assemble {
}
PVE::JSONSchema::register_format('graphite-path', \&pve_verify_graphite_path);
sub pve_verify_graphite_path {
my ($path, $noerr) = @_;
my $regex = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
if ($path !~ /^(${regex}\.)*${regex}$/) {
return undef if $noerr;
die "value does not look like a valid graphite path\n";
return undef if $noerr;
die "value does not look like a valid graphite path\n";
}
return $path;
}
1;

View file

@ -21,64 +21,68 @@ sub type {
sub properties {
return {
organization => {
description => "The InfluxDB organization. Only necessary when using the http v2 api."
." Has no meaning when using v2 compatibility api.",
type => 'string',
optional => 1,
},
bucket => {
description => "The InfluxDB bucket/db. Only necessary when using the http v2 api.",
type => 'string',
optional => 1,
},
token => {
description => "The InfluxDB access token. Only necessary when using the http v2 api."
." If the v2 compatibility api is used, use 'user:password' instead.",
type => 'string',
optional => 1,
},
'api-path-prefix' => {
description => "An API path prefix inserted between '<host>:<port>/' and '/api2/'."
." Can be useful if the InfluxDB service runs behind a reverse proxy.",
type => 'string',
optional => 1,
},
influxdbproto => {
type => 'string',
enum => ['udp', 'http', 'https'],
default => 'udp',
optional => 1,
},
'max-body-size' => {
description => "InfluxDB max-body-size in bytes. Requests are batched up to this size.",
type => 'integer',
minimum => 1,
default => 25_000_000,
},
'verify-certificate' => {
description => "Set to 0 to disable certificate verification for https endpoints.",
type => 'boolean',
optional => 1,
default => 1,
},
organization => {
description =>
"The InfluxDB organization. Only necessary when using the http v2 api."
. " Has no meaning when using v2 compatibility api.",
type => 'string',
optional => 1,
},
bucket => {
description => "The InfluxDB bucket/db. Only necessary when using the http v2 api.",
type => 'string',
optional => 1,
},
token => {
description =>
"The InfluxDB access token. Only necessary when using the http v2 api."
. " If the v2 compatibility api is used, use 'user:password' instead.",
type => 'string',
optional => 1,
},
'api-path-prefix' => {
description => "An API path prefix inserted between '<host>:<port>/' and '/api2/'."
. " Can be useful if the InfluxDB service runs behind a reverse proxy.",
type => 'string',
optional => 1,
},
influxdbproto => {
type => 'string',
enum => ['udp', 'http', 'https'],
default => 'udp',
optional => 1,
},
'max-body-size' => {
description =>
"InfluxDB max-body-size in bytes. Requests are batched up to this size.",
type => 'integer',
minimum => 1,
default => 25_000_000,
},
'verify-certificate' => {
description => "Set to 0 to disable certificate verification for https endpoints.",
type => 'boolean',
optional => 1,
default => 1,
},
};
}
sub options {
return {
server => {},
port => {},
mtu => { optional => 1 },
disable => { optional => 1 },
organization => { optional => 1},
bucket => { optional => 1},
token => { optional => 1},
influxdbproto => { optional => 1},
timeout => { optional => 1},
'max-body-size' => { optional => 1 },
'api-path-prefix' => { optional => 1 },
'verify-certificate' => { optional => 1 },
};
server => {},
port => {},
mtu => { optional => 1 },
disable => { optional => 1 },
organization => { optional => 1 },
bucket => { optional => 1 },
token => { optional => 1 },
influxdbproto => { optional => 1 },
timeout => { optional => 1 },
'max-body-size' => { optional => 1 },
'api-path-prefix' => { optional => 1 },
'verify-certificate' => { optional => 1 },
};
}
my $set_ssl_opts = sub {
@ -86,10 +90,10 @@ my $set_ssl_opts = sub {
my $cert_verify = $cfg->{'verify-certificate'} // 1;
if (!$cert_verify) {
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
);
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
);
}
return;
@ -110,8 +114,8 @@ sub update_qemu_status {
$ctime *= 1000000000;
my $object = "object=qemu,vmid=$vmid,nodename=$nodename";
if($data->{name} && $data->{name} ne '') {
$object .= ",host=$data->{name}";
if ($data->{name} && $data->{name} ne '') {
$object .= ",host=$data->{name}";
}
$object =~ s/\s/\\ /g;
@ -125,8 +129,8 @@ sub update_lxc_status {
$ctime *= 1000000000;
my $object = "object=lxc,vmid=$vmid,nodename=$nodename";
if($data->{name} && $data->{name} ne '') {
$object .= ",host=$data->{name}";
if ($data->{name} && $data->{name} ne '') {
$object .= ",host=$data->{name}";
}
$object =~ s/\s/\\ /g;
@ -140,8 +144,8 @@ sub update_storage_status {
$ctime *= 1000000000;
my $object = "object=storages,nodename=$nodename,host=$storeid";
if($data->{type} && $data->{type} ne '') {
$object .= ",type=$data->{type}";
if ($data->{type} && $data->{type} ne '') {
$object .= ",type=$data->{type}";
}
$object =~ s/\s/\\ /g;
@ -152,7 +156,7 @@ sub _send_batch_size {
my ($class, $cfg) = @_;
my $proto = $cfg->{influxdbproto} // 'udp';
if ($proto ne 'udp') {
return $cfg->{'max-body-size'} // 25_000_000;
return $cfg->{'max-body-size'} // 25_000_000;
}
return $class->SUPER::_send_batch_size($cfg);
@ -163,20 +167,20 @@ sub send {
my $proto = $cfg->{influxdbproto} // 'udp';
if ($proto eq 'udp') {
return $class->SUPER::send($connection, $data, $cfg);
return $class->SUPER::send($connection, $data, $cfg);
} elsif ($proto =~ m/^https?$/) {
my $ua = LWP::UserAgent->new();
$set_ssl_opts->($cfg, $ua);
$ua->timeout($cfg->{timeout} // 1);
$connection->content($data);
my $response = $ua->request($connection);
my $ua = LWP::UserAgent->new();
$set_ssl_opts->($cfg, $ua);
$ua->timeout($cfg->{timeout} // 1);
$connection->content($data);
my $response = $ua->request($connection);
if (!$response->is_success) {
my $err = $response->status_line;
die "$err\n";
}
if (!$response->is_success) {
my $err = $response->status_line;
die "$err\n";
}
} else {
die "invalid protocol\n";
die "invalid protocol\n";
}
return;
@ -186,7 +190,7 @@ sub _disconnect {
my ($class, $connection, $cfg) = @_;
my $proto = $cfg->{influxdbproto} // 'udp';
if ($proto eq 'udp') {
return $class->SUPER::_disconnect($connection, $cfg);
return $class->SUPER::_disconnect($connection, $cfg);
}
return;
@ -197,11 +201,11 @@ sub _get_v2url {
my ($proto, $host, $port) = $cfg->@{qw(influxdbproto server port)};
my $api_prefix = $cfg->{'api-path-prefix'} // '/';
if ($api_prefix ne '/' && $api_prefix =~ m!^/*(.+)/*$!) {
$api_prefix = "/$1/";
$api_prefix = "/$1/";
}
if ($api_path ne 'health') {
$api_path = "api/v2/${api_path}";
$api_path = "api/v2/${api_path}";
}
return "${proto}://${host}:${port}${api_prefix}${api_path}";
@ -215,27 +219,27 @@ sub _connect {
my $proto = $cfg->{influxdbproto} // 'udp';
if ($proto eq 'udp') {
my $socket = IO::Socket::IP->new(
PeerAddr => $host,
PeerPort => $port,
Proto => 'udp',
) || die "couldn't create influxdb socket [$host]:$port - $@\n";
my $socket = IO::Socket::IP->new(
PeerAddr => $host,
PeerPort => $port,
Proto => 'udp',
) || die "couldn't create influxdb socket [$host]:$port - $@\n";
$socket->blocking(0);
$socket->blocking(0);
return $socket;
return $socket;
} elsif ($proto =~ m/^https?$/) {
my $token = get_credentials($id, 1);
my $org = $cfg->{organization} // 'proxmox';
my $bucket = $cfg->{bucket} // 'proxmox';
my $url = _get_v2url($cfg, "write?org=${org}&bucket=${bucket}");
my $token = get_credentials($id, 1);
my $org = $cfg->{organization} // 'proxmox';
my $bucket = $cfg->{bucket} // 'proxmox';
my $url = _get_v2url($cfg, "write?org=${org}&bucket=${bucket}");
my $req = HTTP::Request->new(POST => $url);
if (defined($token)) {
$req->header( "Authorization", "Token $token");
}
my $req = HTTP::Request->new(POST => $url);
if (defined($token)) {
$req->header("Authorization", "Token $token");
}
return $req;
return $req;
}
die "cannot connect to InfluxDB: invalid protocol '$proto'\n";
@ -249,25 +253,25 @@ sub test_connection {
my $proto = $cfg->{influxdbproto} // 'udp';
if ($proto eq 'udp') {
return $class->SUPER::test_connection($cfg, $id);
return $class->SUPER::test_connection($cfg, $id);
} elsif ($proto =~ m/^https?$/) {
my $url = _get_v2url($cfg, "health");
my $ua = LWP::UserAgent->new();
$set_ssl_opts->($cfg, $ua);
$ua->timeout($cfg->{timeout} // 1);
# in the initial add connection test, the token may still be in $cfg
my $token = $cfg->{token} // get_credentials($id, 1);
if (defined($token)) {
$ua->default_header("Authorization" => "Token $token");
}
my $response = $ua->get($url);
my $url = _get_v2url($cfg, "health");
my $ua = LWP::UserAgent->new();
$set_ssl_opts->($cfg, $ua);
$ua->timeout($cfg->{timeout} // 1);
# in the initial add connection test, the token may still be in $cfg
my $token = $cfg->{token} // get_credentials($id, 1);
if (defined($token)) {
$ua->default_header("Authorization" => "Token $token");
}
my $response = $ua->get($url);
if (!$response->is_success) {
my $err = $response->status_line;
die "$err\n";
}
if (!$response->is_success) {
my $err = $response->status_line;
die "$err\n";
}
} else {
die "invalid protocol\n";
die "invalid protocol\n";
}
return;
@ -277,40 +281,42 @@ sub build_influxdb_payload {
my ($class, $txn, $data, $ctime, $tags, $excluded, $measurement, $instance) = @_;
# 'abc' and '123' are both valid hostnames/tags, that confuses influx's type detection
my $to_quote = { name => 1, tags => 1, };
my $to_quote = { name => 1, tags => 1 };
my @values = ();
foreach my $key (sort keys %$data) {
next if defined($excluded) && $excluded->{$key};
my $value = $data->{$key};
next if !defined($value);
next if defined($excluded) && $excluded->{$key};
my $value = $data->{$key};
next if !defined($value);
if (!ref($value) && $value ne '') {
# value is scalar
if (!ref($value) && $value ne '') {
# value is scalar
if (defined(my $v = prepare_value($value, $to_quote->{$key}))) {
push @values, "$key=$v";
}
} elsif (ref($value) eq 'HASH') {
# value is a hash
if (defined(my $v = prepare_value($value, $to_quote->{$key}))) {
push @values, "$key=$v";
}
} elsif (ref($value) eq 'HASH') {
# value is a hash
if (!defined($measurement)) {
build_influxdb_payload($class, $txn, $value, $ctime, $tags, $excluded, $key);
} elsif(!defined($instance)) {
build_influxdb_payload($class, $txn, $value, $ctime, $tags, $excluded, $measurement, $key);
} else {
push @values, get_recursive_values($value);
}
}
if (!defined($measurement)) {
build_influxdb_payload($class, $txn, $value, $ctime, $tags, $excluded, $key);
} elsif (!defined($instance)) {
build_influxdb_payload(
$class, $txn, $value, $ctime, $tags, $excluded, $measurement, $key,
);
} else {
push @values, get_recursive_values($value);
}
}
}
if (@values > 0) {
my $mm = $measurement // 'system';
my $tagstring = $tags;
$tagstring .= ",instance=$instance" if defined($instance);
my $valuestr = join(',', @values);
$class->add_metric_data($txn, "$mm,$tagstring $valuestr $ctime\n");
my $mm = $measurement // 'system';
my $tagstring = $tags;
$tagstring .= ",instance=$instance" if defined($instance);
my $valuestr = join(',', @values);
$class->add_metric_data($txn, "$mm,$tagstring $valuestr $ctime\n");
}
}
@ -320,14 +326,14 @@ sub get_recursive_values {
my @values = ();
foreach my $key (keys %$hash) {
my $value = $hash->{$key};
if(ref($value) eq 'HASH') {
push(@values, get_recursive_values($value));
} elsif (!ref($value) && $value ne '') {
if (defined(my $v = prepare_value($value))) {
push @values, "$key=$v";
}
}
my $value = $hash->{$key};
if (ref($value) eq 'HASH') {
push(@values, get_recursive_values($value));
} elsif (!ref($value) && $value ne '') {
if (defined(my $v = prepare_value($value))) {
push @values, "$key=$v";
}
}
}
return @values;
@ -338,13 +344,13 @@ sub prepare_value {
# don't treat value like a number if quote is 1
if (!$force_quote && looks_like_number($value)) {
if (isnan($value) || isinf($value)) {
# we cannot send influxdb NaN or Inf
return undef;
}
if (isnan($value) || isinf($value)) {
# we cannot send influxdb NaN or Inf
return undef;
}
# influxdb also accepts 1.0e+10, etc.
return $value;
# influxdb also accepts 1.0e+10, etc.
return $value;
}
# non-numeric values require to be quoted, so escape " with \"
@ -365,8 +371,8 @@ sub delete_credentials {
my ($id) = @_;
if (my $cred_file = cred_file_name($id)) {
unlink($cred_file)
or warn "removing influxdb credentials file '$cred_file' failed: $!\n";
unlink($cred_file)
or warn "removing influxdb credentials file '$cred_file' failed: $!\n";
}
return;
@ -399,9 +405,9 @@ sub on_add_hook {
my $token = $sensitive_opts->{token};
if (defined($token)) {
set_credentials($id, $token);
set_credentials($id, $token);
} else {
delete_credentials($id);
delete_credentials($id);
}
return undef;
@ -413,9 +419,9 @@ sub on_update_hook {
my $token = $sensitive_opts->{token};
if (defined($token)) {
set_credentials($id, $token);
set_credentials($id, $token);
} else {
delete_credentials($id);
delete_credentials($id);
}
return undef;
@ -429,5 +435,4 @@ sub on_delete_hook {
return undef;
}
1;

View file

@ -9,44 +9,48 @@ use PVE::SectionConfig;
use base qw(PVE::SectionConfig);
cfs_register_file('status.cfg',
cfs_register_file(
'status.cfg',
sub { __PACKAGE__->parse_config(@_); },
sub { __PACKAGE__->write_config(@_); }
sub { __PACKAGE__->write_config(@_); },
);
my $defaultData = {
propertyList => {
id => {
description => "The ID of the entry.",
type => 'string', format => 'pve-configid',
},
type => {
description => "Plugin type.",
type => 'string', format => 'pve-configid',
},
disable => {
description => "Flag to disable the plugin.",
type => 'boolean',
optional => 1,
},
server => {
type => 'string', format => 'address',
description => "server dns name or IP address",
},
port => {
type => 'integer',
description => "server network port",
minimum => 1,
maximum => 64*1024,
},
mtu => {
type => 'integer',
description => "MTU for metrics transmission over UDP",
default => 1500,
minimum => 512,
maximum => 64*1024,
optional => 1,
},
id => {
description => "The ID of the entry.",
type => 'string',
format => 'pve-configid',
},
type => {
description => "Plugin type.",
type => 'string',
format => 'pve-configid',
},
disable => {
description => "Flag to disable the plugin.",
type => 'boolean',
optional => 1,
},
server => {
type => 'string',
format => 'address',
description => "server dns name or IP address",
},
port => {
type => 'integer',
description => "server network port",
minimum => 1,
maximum => 64 * 1024,
},
mtu => {
type => 'integer',
description => "MTU for metrics transmission over UDP",
default => 1500,
minimum => 512,
maximum => 64 * 1024,
optional => 1,
},
},
};
@ -58,13 +62,13 @@ sub parse_section_header {
my ($class, $line) = @_;
if ($line =~ m/^(\S+):\s*(\S+)?\s*$/) {
my $type = lc($1);
my $id = $2 // $type;
my $errmsg = undef; # set if you want to skip whole section
eval { PVE::JSONSchema::pve_verify_configid($id) };
$errmsg = $@ if $@;
my $config = {}; # to return additional attributes
return ($type, $id, $errmsg, $config);
my $type = lc($1);
my $id = $2 // $type;
my $errmsg = undef; # set if you want to skip whole section
eval { PVE::JSONSchema::pve_verify_configid($id) };
$errmsg = $@ if $@;
my $config = {}; # to return additional attributes
return ($type, $id, $errmsg, $config);
}
return undef;
}
@ -99,7 +103,7 @@ sub add_metric_data {
my $dataq_len = length($txn->{data}) // 0;
if (($dataq_len + $data_length) >= $batch_size) {
$class->flush_data($txn);
$class->flush_data($txn);
}
$txn->{data} //= '';
$txn->{data} .= "$data";
@ -109,8 +113,8 @@ sub flush_data {
my ($class, $txn) = @_;
if (!$txn->{connection}) {
return if !$txn->{data}; # OK, if data was already sent/flushed
die "cannot flush metric data, no connection available!\n";
return if !$txn->{data}; # OK, if data was already sent/flushed
die "cannot flush metric data, no connection available!\n";
}
return if !defined($txn->{data}) || $txn->{data} eq '';
@ -123,7 +127,7 @@ sub send {
my ($class, $connection, $data, $cfg) = @_;
defined($connection->send($data))
or die "failed to send metrics: $!\n";
or die "failed to send metrics: $!\n";
}
sub test_connection {

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ my $pkglist = PVE::APLInfo::load_data();
my $err = 0;
foreach my $k (keys %{$pkglist->{'all'}}) {
foreach my $k (keys %{ $pkglist->{'all'} }) {
next if $k eq 'pve-web-news';
my $res = $pkglist->{all}->{$k};
@ -18,11 +18,11 @@ foreach my $k (keys %{$pkglist->{'all'}}) {
my $template = "$res->{package}_$res->{version}_$res->{architecture}.tar";
if ($k !~ m/^($res->{os}-)?\Q$template\E\.(gz|xz|zst)$/) {
print "ERROR: $k != $template\n";
#print Dumper($res) . "\n";
$err = 1;
print "ERROR: $k != $template\n";
#print Dumper($res) . "\n";
$err = 1;
}
}
$err ? exit (-11) : exit (0);
$err ? exit(-11) : exit(0);

View file

@ -13,13 +13,13 @@ use PVE::Tools;
my $country = {};
my $line;
open (TMP, "</usr/share/zoneinfo/iso3166.tab");
while (defined ($line = <TMP>)) {
open(TMP, "</usr/share/zoneinfo/iso3166.tab");
while (defined($line = <TMP>)) {
if ($line =~ m/^([A-Z][A-Z])\s+(.*\S)\s*$/) {
$country->{lc($1)} = $2;
$country->{ lc($1) } = $2;
}
}
close (TMP);
close(TMP);
# we need mappings for X11, console, and kvm vnc
@ -27,52 +27,51 @@ close (TMP);
my $keymaps = PVE::Tools::kvmkeymaps();
foreach my $km (sort keys %$keymaps) {
my ($desc, $kvm, $console, $x11, $x11var) = @{$keymaps->{$km}};
my ($desc, $kvm, $console, $x11, $x11var) = @{ $keymaps->{$km} };
if ($km =~m/^([a-z][a-z])-([a-z][a-z])$/i) {
defined ($country->{$2}) || die "undefined country code '$2'";
if ($km =~ m/^([a-z][a-z])-([a-z][a-z])$/i) {
defined($country->{$2}) || die "undefined country code '$2'";
} else {
defined ($country->{$km}) || die "undefined country code '$km'";
defined($country->{$km}) || die "undefined country code '$km'";
}
$x11var = '' if !defined ($x11var);
$x11var = '' if !defined($x11var);
print "map:$km:$desc:$kvm:$console:$x11:$x11var:\n";
}
my $defmap = {
'us' => 'en-us',
'be' => 'fr-be',
'br' => 'pt-br',
'ca' => 'en-us',
'dk' => 'dk',
'nl' => 'en-us', # most Dutch people us US layout
'fi' => 'fi',
'fr' => 'fr',
'de' => 'de',
'at' => 'de',
'hu' => 'hu',
'is' => 'is',
'it' => 'it',
'va' => 'it',
'jp' => 'jp',
'lt' => 'lt',
'mk' => 'mk',
'no' => 'no',
'pl' => 'pl',
'pt' => 'pt',
'si' => 'si',
'es' => 'es',
'gi' => 'es',
'ch' => 'de-ch',
'gb' => 'en-gb',
'lu' => 'fr-ch',
'li' => 'de-ch',
'us' => 'en-us',
'be' => 'fr-be',
'br' => 'pt-br',
'ca' => 'en-us',
'dk' => 'dk',
'nl' => 'en-us', # most Dutch people us US layout
'fi' => 'fi',
'fr' => 'fr',
'de' => 'de',
'at' => 'de',
'hu' => 'hu',
'is' => 'is',
'it' => 'it',
'va' => 'it',
'jp' => 'jp',
'lt' => 'lt',
'mk' => 'mk',
'no' => 'no',
'pl' => 'pl',
'pt' => 'pt',
'si' => 'si',
'es' => 'es',
'gi' => 'es',
'ch' => 'de-ch',
'gb' => 'en-gb',
'lu' => 'fr-ch',
'li' => 'de-ch',
};
my $mirrors = PVE::Tools::debmirrors();
foreach my $cc (keys %$mirrors) {
die "undefined country code '$cc'" if !defined ($country->{$cc});
die "undefined country code '$cc'" if !defined($country->{$cc});
}
foreach my $cc (sort keys %$country) {

View file

@ -14,67 +14,64 @@ use Data::Dumper;
# NOTE: not exhausive, reduced to actually required fields!
my $tree = {
nodes => [
{
id => -3,
name => 'pveA',
children => [ 0,1,2,3 ],
type => 'host',
},
{
id => -5,
name => 'pveB',
children => [ 4,5,6,7 ],
type => 'host',
},
{
id => -7,
name => 'pveC',
children => [ 8,9,10,11 ],
type => 'host',
},
{
id => -3,
name => 'pveA',
children => [0, 1, 2, 3],
type => 'host',
},
{
id => -5,
name => 'pveB',
children => [4, 5, 6, 7],
type => 'host',
},
{
id => -7,
name => 'pveC',
children => [8, 9, 10, 11],
type => 'host',
},
],
};
# Check if all the grep and casts are correct
my @belong_to_B = ( 4,5 );
my @not_belong_to_B = ( -1,1,10,15 );
my @belong_to_B = (4, 5);
my @not_belong_to_B = (-1, 1, 10, 15);
foreach (@belong_to_B) {
is (
PVE::API2::Ceph::OSD::osd_belongs_to_node($tree, 'pveB', $_),
1,
"OSD $_ belongs to node pveB",
is(
PVE::API2::Ceph::OSD::osd_belongs_to_node($tree, 'pveB', $_),
1,
"OSD $_ belongs to node pveB",
);
}
foreach (@not_belong_to_B) {
is (
PVE::API2::Ceph::OSD::osd_belongs_to_node($tree, 'pveB', $_),
0,
"OSD $_ does not belong to node pveB",
is(
PVE::API2::Ceph::OSD::osd_belongs_to_node($tree, 'pveB', $_),
0,
"OSD $_ does not belong to node pveB",
);
}
my $double_nodes_tree = {
nodes => [
{
name => 'pveA',
type => 'host',
},
{
name => 'pveA',
type => 'host',
}
]
{
name => 'pveA',
type => 'host',
},
{
name => 'pveA',
type => 'host',
},
],
};
eval { PVE::API2::Ceph::OSD::osd_belongs_to_node($double_nodes_tree, 'pveA') };
like($@, qr/duplicate host name found/, "Die if node occurs too often");
is (
is(
PVE::API2::Ceph::OSD::osd_belongs_to_node(undef),
0,
"Early-return false if there's no/empty node tree",
);
done_testing(@belong_to_B + @not_belong_to_B + 2);

View file

@ -35,9 +35,9 @@ our $mocked_ct_configs = {};
my $mocked_get_members = sub {
return {
node1 => { online => 1 },
node2 => { online => 1 },
node3 => { online => 1 },
node1 => { online => 1 },
node2 => { online => 1 },
node3 => { online => 1 },
};
};
@ -45,18 +45,18 @@ my $mocked_vmlist = sub {
my $res = {};
foreach my $id (keys %$mocked_ct_configs) {
my $d = $mocked_ct_configs->{$id};
$res->{$id} = { 'type' => 'lxc', 'node' => $d->{node}, 'version' => 1 };
my $d = $mocked_ct_configs->{$id};
$res->{$id} = { 'type' => 'lxc', 'node' => $d->{node}, 'version' => 1 };
}
foreach my $id (keys %$mocked_vm_configs) {
my $d = $mocked_vm_configs->{$id};
$res->{$id} = { 'type' => 'qemu', 'node' => $d->{node}, 'version' => 1 };
my $d = $mocked_vm_configs->{$id};
$res->{$id} = { 'type' => 'qemu', 'node' => $d->{node}, 'version' => 1 };
}
return { 'ids' => $res };
};
my $mocked_get_ssh_info = sub {
my $mocked_get_ssh_info = sub {
my ($node, $network_cidr) = @_;
return { node => $node };
@ -129,24 +129,24 @@ my $mocked_replication_config_new = sub {
my $mocked_storage_config = {
ids => {
local => {
type => 'dir',
shared => 0,
content => {
'iso' => 1,
'backup' => 1,
},
path => "/var/lib/vz",
},
'local-zfs' => {
type => 'zfspool',
pool => 'nonexistent-testpool',
shared => 0,
content => {
'images' => 1,
'rootdir' => 1
},
},
local => {
type => 'dir',
shared => 0,
content => {
'iso' => 1,
'backup' => 1,
},
path => "/var/lib/vz",
},
'local-zfs' => {
type => 'zfspool',
pool => 'nonexistent-testpool',
shared => 0,
content => {
'images' => 1,
'rootdir' => 1,
},
},
},
};
@ -160,8 +160,8 @@ sub generate_snapshot_info {
$timestamp_counter++;
return {
id => $timestamp_counter,
timestamp => $timestamp_counter,
id => $timestamp_counter,
timestamp => $timestamp_counter,
};
}
@ -169,8 +169,8 @@ sub register_mocked_volid {
my ($volid, $snapname) = @_;
my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid);
my $scfg = $mocked_storage_config->{ids}->{$storeid} ||
die "no such storage '$storeid'\n";
my $scfg = $mocked_storage_config->{ids}->{$storeid}
|| die "no such storage '$storeid'\n";
my $d = $mocked_storage_content->{$storeid}->{$volname} //= {};
@ -261,9 +261,9 @@ sub setup {
$pve_storage_module->mock(volume_snapshot_info => $mocked_volume_snapshot_info);
$pve_replication_config_module->mock(
new => $mocked_replication_config_new,
lock => sub { $mocked_cfs_lock_file->('replication.cfg', undef, $_[0]); },
write => sub { $mocked_cfs_write_file->('replication.cfg', $_[0]); },
new => $mocked_replication_config_new,
lock => sub { $mocked_cfs_lock_file->('replication.cfg', undef, $_[0]); },
write => sub { $mocked_cfs_write_file->('replication.cfg', $_[0]); },
);
$pve_qemuserver_module->mock(check_running => sub { return 0; });
$pve_qemuconfig_module->mock(load_config => $mocked_qemu_load_conf);
@ -271,20 +271,20 @@ sub setup {
$pve_lxc_config_module->mock(load_config => $mocked_lxc_load_conf);
$pve_sshinfo_module->mock(
get_ssh_info => $mocked_get_ssh_info,
ssh_info_to_command => $mocked_ssh_info_to_command,
get_ssh_info => $mocked_get_ssh_info,
ssh_info_to_command => $mocked_ssh_info_to_command,
);
$pve_cluster_module->mock(
get_vmlist => sub { return $mocked_vmlist->(); },
get_members => $mocked_get_members,
cfs_update => sub {},
cfs_lock_file => $mocked_cfs_lock_file,
cfs_write_file => $mocked_cfs_write_file,
cfs_read_file => $mocked_cfs_read_file,
get_vmlist => sub { return $mocked_vmlist->(); },
get_members => $mocked_get_members,
cfs_update => sub { },
cfs_lock_file => $mocked_cfs_lock_file,
cfs_write_file => $mocked_cfs_write_file,
cfs_read_file => $mocked_cfs_read_file,
);
$pve_inotify_module->mock('nodename' => sub { return $mocked_nodename; });
};
}
# code to generate/conpare test logs
@ -295,19 +295,19 @@ sub openlog {
my ($filename) = @_;
if (!$filename) {
# compute from $0
$filename = basename($0);
if ($filename =~ m/^(\S+)\.pl$/) {
$filename = "$1.log";
} else {
die "unable to compute log name for $0";
}
# compute from $0
$filename = basename($0);
if ($filename =~ m/^(\S+)\.pl$/) {
$filename = "$1.log";
} else {
die "unable to compute log name for $0";
}
}
die "log already open" if defined($logname);
open (my $fh, ">", "$filename.tmp") ||
die "unable to open log - $!";
open(my $fh, ">", "$filename.tmp")
|| die "unable to open log - $!";
$logname = $filename;
$logfh = $fh;
@ -318,15 +318,15 @@ sub commit_log {
close($logfh);
if (-f $logname) {
my $diff = `diff -u '$logname' '$logname.tmp'`;
if ($diff) {
warn "got unexpected output\n";
print "# diff -u '$logname' '$logname.tmp'\n";
print $diff;
exit(-1);
}
my $diff = `diff -u '$logname' '$logname.tmp'`;
if ($diff) {
warn "got unexpected output\n";
print "# diff -u '$logname' '$logname.tmp'\n";
print $diff;
exit(-1);
}
} else {
rename("$logname.tmp", $logname) || die "rename log failed - $!";
rename("$logname.tmp", $logname) || die "rename log failed - $!";
}
}
@ -339,18 +339,18 @@ sub track_jobs {
$mocked_log_time = $ctime;
my $logmsg = sub {
my ($msg) = @_;
my ($msg) = @_;
print "$msg\n";
print $logfh "$msg\n";
print "$msg\n";
print $logfh "$msg\n";
};
if (!$status) {
$status = PVE::ReplicationState::job_status();
foreach my $jobid (sort keys %$status) {
my $jobcfg = $status->{$jobid};
$logmsg->("$ctime $jobid: new job next_sync => $jobcfg->{next_sync}");
}
$status = PVE::ReplicationState::job_status();
foreach my $jobid (sort keys %$status) {
my $jobcfg = $status->{$jobid};
$logmsg->("$ctime $jobid: new job next_sync => $jobcfg->{next_sync}");
}
}
PVE::API2::Replication::run_jobs($ctime, $logmsg, 1);
@ -359,62 +359,61 @@ sub track_jobs {
# detect removed jobs
foreach my $jobid (sort keys %$status) {
if (!$new->{$jobid}) {
$logmsg->("$ctime $jobid: vanished job");
}
if (!$new->{$jobid}) {
$logmsg->("$ctime $jobid: vanished job");
}
}
foreach my $jobid (sort keys %$new) {
my $jobcfg = $new->{$jobid};
my $oldcfg = $status->{$jobid};
if (!$oldcfg) {
$logmsg->("$ctime $jobid: new job next_sync => $jobcfg->{next_sync}");
next; # no old state to compare
} else {
foreach my $k (qw(target guest vmtype next_sync)) {
my $changes = '';
if ($oldcfg->{$k} ne $jobcfg->{$k}) {
$changes .= ', ' if $changes;
$changes .= "$k => $jobcfg->{$k}";
}
$logmsg->("$ctime $jobid: changed config $changes") if $changes;
}
}
my $jobcfg = $new->{$jobid};
my $oldcfg = $status->{$jobid};
if (!$oldcfg) {
$logmsg->("$ctime $jobid: new job next_sync => $jobcfg->{next_sync}");
next; # no old state to compare
} else {
foreach my $k (qw(target guest vmtype next_sync)) {
my $changes = '';
if ($oldcfg->{$k} ne $jobcfg->{$k}) {
$changes .= ', ' if $changes;
$changes .= "$k => $jobcfg->{$k}";
}
$logmsg->("$ctime $jobid: changed config $changes") if $changes;
}
}
my $oldstate = $oldcfg->{state};
my $oldstate = $oldcfg->{state};
my $state = $jobcfg->{state};
my $state = $jobcfg->{state};
my $changes = '';
foreach my $k (qw(last_node last_try last_sync fail_count error)) {
if (($oldstate->{$k} // '') ne ($state->{$k} // '')) {
my $value = $state->{$k} // '';
chomp $value;
$changes .= ', ' if $changes;
$changes .= "$k => $value";
}
}
$logmsg->("$ctime $jobid: changed state $changes") if $changes;
my $changes = '';
foreach my $k (qw(last_node last_try last_sync fail_count error)) {
if (($oldstate->{$k} // '') ne ($state->{$k} // '')) {
my $value = $state->{$k} // '';
chomp $value;
$changes .= ', ' if $changes;
$changes .= "$k => $value";
}
}
$logmsg->("$ctime $jobid: changed state $changes") if $changes;
my $old_storeid_list = $oldstate->{storeid_list};
my $storeid_list = $state->{storeid_list};
my $old_storeid_list = $oldstate->{storeid_list};
my $storeid_list = $state->{storeid_list};
my $storeid_list_changes = 0;
foreach my $storeid (@$storeid_list) {
next if grep { $_ eq $storeid } @$old_storeid_list;
$storeid_list_changes = 1;
}
my $storeid_list_changes = 0;
foreach my $storeid (@$storeid_list) {
next if grep { $_ eq $storeid } @$old_storeid_list;
$storeid_list_changes = 1;
}
foreach my $storeid (@$old_storeid_list) {
next if grep { $_ eq $storeid } @$storeid_list;
$storeid_list_changes = 1;
}
foreach my $storeid (@$old_storeid_list) {
next if grep { $_ eq $storeid } @$storeid_list;
$storeid_list_changes = 1;
}
$logmsg->("$ctime $jobid: changed storeid list " . join(',', @$storeid_list))
if $storeid_list_changes;
$logmsg->("$ctime $jobid: changed storeid list " . join(',', @$storeid_list))
if $storeid_list_changes;
}
$status = $new;
}
1;

View file

@ -10,18 +10,18 @@ my $debug = 0;
my $test_status1 = {
100 => {
maxmem => GB(2),
shares => 2000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
maxmem => GB(2),
shares => 2000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
},
101 => {
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
},
};
@ -36,18 +36,18 @@ absim($test_status1, MB(900), 100 => MB(1600), 101 => MB(1300));
my $test_status2 = {
100 => {
maxmem => GB(2),
shares => 2000,
balloon => GB(2),
balloon_min => GB(2),
freemem => MB(0),
maxmem => GB(2),
shares => 2000,
balloon => GB(2),
balloon_min => GB(2),
freemem => MB(0),
},
101 => {
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(0),
},
};
@ -57,38 +57,38 @@ abtest($test_status2, MB(500), 101 => MB(1100));
my $test_status3 = {
100 => {
maxmem => GB(2),
shares => 2000,
balloon => GB(2),
balloon_min => GB(2),
freemem => MB(0),
maxmem => GB(2),
shares => 2000,
balloon => GB(2),
balloon_min => GB(2),
freemem => MB(0),
},
101 => {
maxmem => GB(2),
shares => 1000,
balloon => GB(1)+MB(7),
balloon_min => GB(1),
freemem => MB(0),
maxmem => GB(2),
shares => 1000,
balloon => GB(1) + MB(7),
balloon_min => GB(1),
freemem => MB(0),
},
102 => {
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(512),
maxmem => GB(2),
shares => 1000,
balloon => GB(1),
balloon_min => GB(1),
freemem => MB(512),
},
};
abtest($test_status3, 0);
abtest($test_status3, MB(11), 101 => MB(1018));
abtest($test_status3, MB(80), 101 => MB(1087));
abtest($test_status3, MB(200), 101 => MB(1107));
abtest($test_status3, MB(11), 101 => MB(1018));
abtest($test_status3, MB(80), 101 => MB(1087));
abtest($test_status3, MB(200), 101 => MB(1107));
my $status = absim($test_status3, MB(593), 101 => MB(1300), 102 => MB(1300));
my $status = absim($test_status3, MB(593), 101 => MB(1300), 102 => MB(1300));
absim($status, -MB(200), 101 => MB(1200), 102 => MB(1200));
absim($status, -MB(400), 101 => MB(1200), 102 => GB(1));
absim($status, -MB(593), 101 => MB(1007), 102 => GB(1));
exit (0);
exit(0);
sub abapply {
my ($vmstatus, $res, $sum) = @_;
@ -96,39 +96,40 @@ sub abapply {
my $changes = 0;
my $abschanges = 0;
foreach my $vmid (keys %$res) {
my $diff = $res->{$vmid} - $vmstatus->{$vmid}->{balloon};
if ($diff != 0) {
# fixme: adjust freemem ?
$vmstatus->{$vmid}->{freemem} += $diff;
$vmstatus->{$vmid}->{freemem} = 0 if $vmstatus->{$vmid}->{freemem} < 0;
$vmstatus->{$vmid}->{balloon} = $res->{$vmid};
$sum->{$vmid} = $res->{$vmid};
$changes += $diff;
$abschanges += $diff > 0 ? $diff : -$diff;
}
my $diff = $res->{$vmid} - $vmstatus->{$vmid}->{balloon};
if ($diff != 0) {
# fixme: adjust freemem ?
$vmstatus->{$vmid}->{freemem} += $diff;
$vmstatus->{$vmid}->{freemem} = 0 if $vmstatus->{$vmid}->{freemem} < 0;
$vmstatus->{$vmid}->{balloon} = $res->{$vmid};
$sum->{$vmid} = $res->{$vmid};
$changes += $diff;
$abschanges += $diff > 0 ? $diff : -$diff;
}
}
return ($changes, $abschanges);
}
my $tcount = 0;
sub absim {
my ($vmstatus, $goal, %expect) = @_;
$tcount++;
print "BALLOON SIM $tcount\n" if $debug;
$vmstatus = dclone($vmstatus); # do not change original
my $changes = 0;
my $abschanges = 0;
my $sum = {};
do {
my $res = PVE::AutoBalloon::compute_alg1($vmstatus, $goal, MB(100), $debug);
print Dumper($res) if $debug;
($changes, $abschanges) = abapply($vmstatus, $res, $sum);
$goal -= $changes;
my $res = PVE::AutoBalloon::compute_alg1($vmstatus, $goal, MB(100), $debug);
print Dumper($res) if $debug;
($changes, $abschanges) = abapply($vmstatus, $res, $sum);
$goal -= $changes;
} while ($abschanges);
abcheck($sum, %expect);
@ -143,19 +144,19 @@ sub abcheck {
my ($res, %expect) = @_;
foreach my $vmid (keys %expect) {
my $ev = $expect{$vmid};
if (defined ($res->{$vmid})) {
die "T$tcount: wrong value for VM $vmid ($ev != $res->{$vmid})\n"
if $ev != $res->{$vmid};
} else {
die "T$tcount: missing value for VM $vmid (extected $ev)\n";
}
my $ev = $expect{$vmid};
if (defined($res->{$vmid})) {
die "T$tcount: wrong value for VM $vmid ($ev != $res->{$vmid})\n"
if $ev != $res->{$vmid};
} else {
die "T$tcount: missing value for VM $vmid (extected $ev)\n";
}
}
foreach my $vmid (keys %$res) {
die "T$tcount: got unexpected result for $vmid\n"
if (defined($res->{$vmid}) &&
!defined($expect{$vmid}));
die "T$tcount: got unexpected result for $vmid\n"
if (defined($res->{$vmid})
&& !defined($expect{$vmid}));
}
}
@ -177,9 +178,10 @@ sub abtest {
sub MB {
my $mb = shift;
return $mb*1000*1000;
};
return $mb * 1000 * 1000;
}
sub GB {
my $gb = shift;
return $gb*1000*1000*1000;
};
return $gb * 1000 * 1000 * 1000;
}

View file

@ -23,45 +23,48 @@ sub test_rpc {
my ($host) = @_;
for (my $i = 0; $i < $qcount; $i++) {
eval {
my ($page, $response, %reply_headers)
= get_https($host, 8006, '/api2/json',
make_headers(Cookie => "PVEAuthCookie=$ticket"));
die "$response\n" if $response !~ m/200 OK/;
};
eval {
my ($page, $response, %reply_headers) = get_https(
$host,
8006,
'/api2/json',
make_headers(Cookie => "PVEAuthCookie=$ticket"),
);
die "$response\n" if $response !~ m/200 OK/;
};
my $err = $@;
my $err = $@;
if ($err) {
if ($err) {
print "ERROR: $err\n";
last;
}
print "ERROR: $err\n";
last;
}
}
}
sub run_tests {
my ($host) = @_;
my $workers;
my $starttime = [gettimeofday];
for (my $i = 0; $i < $wcount; $i++) {
if (my $pid = fork ()) {
$workers->{$pid} = 1;
} else {
test_rpc ($host);
exit (0);
}
if (my $pid = fork()) {
$workers->{$pid} = 1;
} else {
test_rpc($host);
exit(0);
}
}
# wait for children
1 while (wait > 0);
my $elapsed = int(tv_interval ($starttime) * 1000);
my $elapsed = int(tv_interval($starttime) * 1000);
my $tpq = $elapsed / ($wcount*$qcount);
my $tpq = $elapsed / ($wcount * $qcount);
print "$host: $tpq ms per query\n";
}

View file

@ -15,7 +15,7 @@ use Test::More tests => 3;
$ReplicationTestEnv::mocked_nodename = 'node1';
my $testjob = {
'type' => 'local',
'type' => 'local',
'target' => 'node1',
'guest' => 900,
};
@ -26,11 +26,11 @@ $ReplicationTestEnv::mocked_replication_jobs = {
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};
@ -39,8 +39,8 @@ ReplicationTestEnv::setup();
ok(PVE::INotify::nodename() eq 'node1');
my $list = PVE::Cluster::get_vmlist();
is_deeply($list, { ids => {900 => { node => 'node1', type => 'qemu', version => 1}}});
is_deeply($list, { ids => { 900 => { node => 'node1', type => 'qemu', version => 1 } } });
my $cfg = PVE::ReplicationConfig->new();
is_deeply($cfg, { ids => { job_900_to_node1 => $testjob }});
is_deeply($cfg, { ids => { job_900_to_node1 => $testjob } });
exit(0);

View file

@ -21,14 +21,15 @@ my $schedule = [];
my $mocked_replicate = sub {
my ($guest_class, $jobcfg, $state, $start_time, $logfunc) = @_;
push @$schedule, {
id => $jobcfg->{id},
guest => $jobcfg->{guest},
vmtype => $jobcfg->{vmtype},
guest_class => $guest_class,
last_sync => $state->{last_sync},
start => $start_time,
};
push @$schedule,
{
id => $jobcfg->{id},
guest => $jobcfg->{guest},
vmtype => $jobcfg->{vmtype},
guest_class => $guest_class,
last_sync => $state->{last_sync},
start => $start_time,
};
};
my $pve_replication_module = Test::MockModule->new('PVE::Replication');
@ -36,66 +37,66 @@ $pve_replication_module->mock(replicate => $mocked_replicate);
$ReplicationTestEnv::mocked_replication_jobs = {
'900-1_to_node2' => {
'type' => 'local',
'target' => 'node2',
'guest' => 900,
'type' => 'local',
'target' => 'node2',
'guest' => 900,
},
'900-2_to_node1' => {
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
},
};
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};
ReplicationTestEnv::setup();
for (my $i = 0; $i < 61; $i++) {
PVE::API2::Replication::run_jobs($i*60);
PVE::API2::Replication::run_jobs($i * 60);
}
my $exptected_schedule = [
{
last_sync => 0,
start => 900,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900
last_sync => 0,
start => 900,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900,
},
{
last_sync => 900,
start => 1800,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900,
},
{
last_sync => 1800,
start => 2700,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900
last_sync => 900,
start => 1800,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900,
},
{
last_sync => 2700,
start => 3600,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900
}
last_sync => 1800,
start => 2700,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900,
},
{
last_sync => 2700,
start => 3600,
vmtype => 'qemu',
guest_class => 'PVE::QemuConfig',
id => '900-1_to_node2',
guest => 900,
},
];
is_deeply($schedule, $exptected_schedule);

View file

@ -18,26 +18,26 @@ use PVE::API2::Replication;
$ReplicationTestEnv::mocked_nodename = 'node1';
my $testjob = {
'type' => 'local',
'type' => 'local',
'target' => 'node1',
'guest' => 900,
};
$ReplicationTestEnv::mocked_replication_jobs = {
job_900_to_node1 => {
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
job_900_to_node1 => {
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
},
};
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};

View file

@ -16,35 +16,34 @@ use PVE::Tools;
$ReplicationTestEnv::mocked_nodename = 'node1';
my $pve_replication_module = Test::MockModule->new('PVE::Replication');
$pve_replication_module->mock(
replicate => sub { die "faked replication error\n"; });
$pve_replication_module->mock(replicate => sub { die "faked replication error\n"; });
my $testjob = {
'type' => 'local',
'type' => 'local',
'target' => 'node1',
'guest' => 900,
};
$ReplicationTestEnv::mocked_replication_jobs = {
job_900_to_node2 => {
'type' => 'local',
'target' => 'node2',
'guest' => 900,
'type' => 'local',
'target' => 'node2',
'guest' => 900,
},
job_900_to_node1 => {
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
'type' => 'local',
'target' => 'node1', # local node, job should be skipped
'guest' => 900,
},
};
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-lvm:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};

View file

@ -30,7 +30,8 @@ use PVE::Storage;
my $replicated_volume_status = {};
my $mocked_remote_prepare_local_job = sub {
my ($ssh_info, $jobid, $vmid, $volumes, $storeid_list, $last_sync, $parent_snapname, $force) = @_;
my ($ssh_info, $jobid, $vmid, $volumes, $storeid_list, $last_sync, $parent_snapname, $force)
= @_;
my $target = $ssh_info->{node};
@ -38,16 +39,17 @@ my $mocked_remote_prepare_local_job = sub {
return $last_snapshots if !defined($replicated_volume_status->{$target});
my $last_sync_snapname = PVE::ReplicationState::replication_snapshot_name($jobid, $last_sync);
my $last_sync_snapname =
PVE::ReplicationState::replication_snapshot_name($jobid, $last_sync);
foreach my $volid (keys %{$replicated_volume_status->{$target}}) {
if (!grep { $_ eq $volid } @$volumes) {
delete $replicated_volume_status->{$target}->{$volid};
next;
}
my $snapname = $replicated_volume_status->{$target}->{$volid};
foreach my $volid (keys %{ $replicated_volume_status->{$target} }) {
if (!grep { $_ eq $volid } @$volumes) {
delete $replicated_volume_status->{$target}->{$volid};
next;
}
my $snapname = $replicated_volume_status->{$target}->{$volid};
$last_snapshots->{$volid}->{$snapname} = 1 if $last_sync_snapname eq $snapname;
$last_snapshots->{$volid}->{$snapname} = 1 if $last_sync_snapname eq $snapname;
}
return $last_snapshots;
@ -80,29 +82,30 @@ my $pve_replication_module = Test::MockModule->new('PVE::Replication');
$pve_replication_module->mock(
remote_prepare_local_job => $mocked_remote_prepare_local_job,
remote_finalize_local_job => $mocked_remote_finalize_local_job,
replicate_volume => $mocked_replicate_volume);
replicate_volume => $mocked_replicate_volume,
);
my $testjob = {
'type' => 'local',
'type' => 'local',
'target' => 'node1',
'guest' => 900,
};
$ReplicationTestEnv::mocked_replication_jobs = {
job_900_to_node2 => {
'type' => 'local',
'target' => 'node2',
'guest' => 900,
'type' => 'local',
'target' => 'node2',
'guest' => 900,
},
};
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-zfs:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-zfs:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};
@ -122,7 +125,7 @@ for (my $i = 0; $i < 15; $i++) {
}
# add a new, disk (but disk does not exist, so replication fails)
$ReplicationTestEnv::mocked_vm_configs->{900}->{ide1} = 'local-zfs:vm-900-disk-2,size=4G';
$ReplicationTestEnv::mocked_vm_configs->{900}->{ide1} = 'local-zfs:vm-900-disk-2,size=4G';
for (my $i = 0; $i < 15; $i++) {
ReplicationTestEnv::track_jobs($ctime);
$ctime += 60;
@ -142,8 +145,6 @@ for (my $i = 0; $i < 15; $i++) {
$ctime += 60;
}
ReplicationTestEnv::commit_log();
exit(0);

View file

@ -20,31 +20,30 @@ my $mocked_delete_job = sub {
};
my $pve_replication_config_module = Test::MockModule->new('PVE::ReplicationConfig');
$pve_replication_config_module->mock(
delete_job => $mocked_delete_job);
$pve_replication_config_module->mock(delete_job => $mocked_delete_job);
my $testjob = {
'type' => 'local',
'type' => 'local',
'target' => 'node1',
'guest' => 900,
};
$ReplicationTestEnv::mocked_replication_jobs = {
job_900_to_node1 => {
remove_job => 'full',
type => 'local',
target => 'node1', # local node, job should be skipped
guest => 900,
remove_job => 'full',
type => 'local',
target => 'node1', # local node, job should be skipped
guest => 900,
},
};
$ReplicationTestEnv::mocked_vm_configs = {
900 => {
node => 'node1',
snapshots => {},
ide0 => 'local-zfs:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
node => 'node1',
snapshots => {},
ide0 => 'local-zfs:vm-900-disk-1,size=4G',
memory => 512,
ide2 => 'none,media=cdrom',
},
};

View file

@ -14,39 +14,39 @@ use Data::Dumper;
my $vmlist = {
'ids' => {
100 => {
'type' => 'qemu',
'node' => 'node1',
},
101 => {
'type' => 'qemu',
'node' => 'node1',
},
112 => {
'type' => 'lxc',
'node' => 'node1',
},
113 => {
'type' => 'lxc',
'node' => 'node1',
},
200 => {
'type' => 'qemu',
'node' => 'node2',
},
201 => {
'type' => 'qemu',
'node' => 'node2',
},
212 => {
'type' => 'lxc',
'node' => 'node2',
},
213 => {
'type' => 'lxc',
'node' => 'node2',
},
}
100 => {
'type' => 'qemu',
'node' => 'node1',
},
101 => {
'type' => 'qemu',
'node' => 'node1',
},
112 => {
'type' => 'lxc',
'node' => 'node1',
},
113 => {
'type' => 'lxc',
'node' => 'node1',
},
200 => {
'type' => 'qemu',
'node' => 'node2',
},
201 => {
'type' => 'qemu',
'node' => 'node2',
},
212 => {
'type' => 'lxc',
'node' => 'node2',
},
213 => {
'type' => 'lxc',
'node' => 'node2',
},
},
};
my $pools = {
@ -56,135 +56,164 @@ my $pools = {
my $pve_cluster_module = Test::MockModule->new('PVE::Cluster');
$pve_cluster_module->mock(
get_vmlist => sub {
return $vmlist;
}
return $vmlist;
},
);
my $pve_inotify = Test::MockModule->new('PVE::INotify');
$pve_inotify->mock(
nodename => sub {
return 'node1';
}
return 'node1';
},
);
my $pve_api2tools = Test::MockModule->new('PVE::API2Tools');
$pve_api2tools->mock(
get_resource_pool_guest_members => sub {
return $pools->{testpool};
}
return $pools->{testpool};
},
);
my $tests = [];
my $addtest = sub {
my ($name, $test) = @_;
push @$tests, {
name => $name,
test => $test,
};
push @$tests,
{
name => $name,
test => $test,
};
};
$addtest->('Test all guests', {
expected => {
node1 => [ 100, 101, 112, 113 ],
node2 => [ 200, 201, 212, 213 ],
$addtest->(
'Test all guests',
{
expected => {
node1 => [100, 101, 112, 113],
node2 => [200, 201, 212, 213],
},
param => {
all => 1,
},
},
param => {
all => 1,
}
});
);
$addtest->('Test all guests with node limit', {
expected => {
node2 => [ 200, 201, 212, 213 ],
$addtest->(
'Test all guests with node limit',
{
expected => {
node2 => [200, 201, 212, 213],
},
param => {
all => 1,
node => 'node2',
},
},
param => {
all => 1,
node => 'node2',
}
});
);
$addtest->('Test exclude', {
expected => {
node1 =>[ 101, 112, 113 ],
node2 => [ 201, 212, 213 ],
$addtest->(
'Test exclude',
{
expected => {
node1 => [101, 112, 113],
node2 => [201, 212, 213],
},
param => {
all => 1,
exclude => '100, 102, 200, 202',
},
},
param => {
all => 1,
exclude => '100, 102, 200, 202',
}
});
);
$addtest->('Test exclude with node limit', {
expected => {
node1 =>[ 101, 112, 113 ],
$addtest->(
'Test exclude with node limit',
{
expected => {
node1 => [101, 112, 113],
},
param => {
all => 1,
exclude => '100, 102, 200, 202',
node => 'node1',
},
},
param => {
all => 1,
exclude => '100, 102, 200, 202',
node => 'node1',
}
});
);
$addtest->('Test pool members', {
expected => {
node1 =>[ 100, 101 ],
node2 => [ 200, 201 ],
$addtest->(
'Test pool members',
{
expected => {
node1 => [100, 101],
node2 => [200, 201],
},
param => {
pool => 'testpool',
},
},
param => {
pool => 'testpool',
}
});
);
$addtest->('Test pool members with node limit', {
expected => {
node2 => [ 200, 201 ],
$addtest->(
'Test pool members with node limit',
{
expected => {
node2 => [200, 201],
},
param => {
pool => 'testpool',
node => 'node2',
},
},
param => {
pool => 'testpool',
node => 'node2',
}
});
);
$addtest->('Test selected VMIDs', {
expected => {
node1 =>[ 100 ],
node2 => [ 201, 212 ],
$addtest->(
'Test selected VMIDs',
{
expected => {
node1 => [100],
node2 => [201, 212],
},
param => {
vmid => '100, 201, 212',
},
},
param => {
vmid => '100, 201, 212',
}
});
);
$addtest->('Test selected VMIDs with node limit', {
expected => {
node1 =>[ 100 ],
$addtest->(
'Test selected VMIDs with node limit',
{
expected => {
node1 => [100],
},
param => {
vmid => '100, 201, 212',
node => 'node1',
},
},
param => {
vmid => '100, 201, 212',
node => 'node1',
}
});
);
$addtest->('Test selected VMIDs on other nodes', {
expected => {
$addtest->(
'Test selected VMIDs on other nodes',
{
expected => {},
param => {
vmid => '201, 212',
node => 'node1',
},
},
param => {
vmid => '201, 212',
node => 'node1',
}
});
$addtest->('Test VMID not present in vmlist', {
expected => {
node1 => [ 100 ],
node2 => [ 201, 212 ],
'' => [ 7654 ],
},
param => {
vmid => '100, 201, 212, 7654',
}
});
);
$addtest->(
'Test VMID not present in vmlist',
{
expected => {
node1 => [100],
node2 => [201, 212],
'' => [7654],
},
param => {
vmid => '100, 201, 212, 7654',
},
},
);
for my $test (@{$tests}) {
my $testname = $test->{name};
@ -192,7 +221,7 @@ for my $test (@{$tests}) {
# note($testname);
my $result = PVE::VZDump::get_included_guests($testdata->{param});
my $result = PVE::VZDump::get_included_guests($testdata->{param});
is_deeply($result, $testdata->{expected}, $testname);
}

File diff suppressed because it is too large Load diff

View file

@ -4,17 +4,18 @@
# This can also be added as a line in /etc/vzdump.conf
# e.g. 'script: /usr/local/bin/vzdump-hook-script.pl'
use strict;
print "HOOK: " . join (' ', @ARGV) . "\n";
print "HOOK: " . join(' ', @ARGV) . "\n";
my $phase = shift;
if ($phase eq 'job-init' ||
$phase eq 'job-start' ||
$phase eq 'job-end' ||
$phase eq 'job-abort') {
if (
$phase eq 'job-init'
|| $phase eq 'job-start'
|| $phase eq 'job-end'
|| $phase eq 'job-abort'
) {
# undef for Proxmox Backup Server storages
# undef in phase 'job-init' except when --dumpdir is used directly
@ -30,21 +31,23 @@ if ($phase eq 'job-init' ||
# example: wake up remote storage node and enable storage
if ($phase eq 'job-init') {
#system("wakeonlan AA:BB:CC:DD:EE:FF");
#sleep(30);
#system ("/sbin/pvesm set $storeid --disable 0") == 0 ||
# die "enabling storage $storeid failed";
#system("wakeonlan AA:BB:CC:DD:EE:FF");
#sleep(30);
#system ("/sbin/pvesm set $storeid --disable 0") == 0 ||
# die "enabling storage $storeid failed";
}
# do what you want
} elsif ($phase eq 'backup-start' ||
$phase eq 'backup-end' ||
$phase eq 'backup-abort' ||
$phase eq 'log-end' ||
$phase eq 'pre-stop' ||
$phase eq 'pre-restart' ||
$phase eq 'post-restart') {
} elsif (
$phase eq 'backup-start'
|| $phase eq 'backup-end'
|| $phase eq 'backup-abort'
|| $phase eq 'log-end'
|| $phase eq 'pre-stop'
|| $phase eq 'pre-restart'
|| $phase eq 'post-restart'
) {
my $mode = shift; # stop/suspend/snapshot
@ -69,7 +72,7 @@ if ($phase eq 'job-init' ||
print "HOOK-ENV: ";
for my $var (qw(vmtype dumpdir storeid hostname target logfile)) {
print "$var=$ENV{uc($var)};" if defined($ENV{uc($var)});
print "$var=$ENV{uc($var)};" if defined($ENV{ uc($var) });
}
print "\n";
@ -91,4 +94,4 @@ if ($phase eq 'job-init' ||
}
exit (0);
exit(0);