pve-manager/PVE/API2/Cluster.pm
Hannes Laimer ac53f1f76c api: cluster/resources: add 'name' property to return schema
VM names are returned by the endpoint anyway, therefore it makes sense
to add it to the endpoint specification so it also appears in the API
docs and is visible when using pvesh with text output.

Signed-off-by: Hannes Laimer <h.laimer@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-07-02 11:52:09 +02:00

744 lines
18 KiB
Perl

package PVE::API2::Cluster;
use strict;
use warnings;
use JSON;
use PVE::API2Tools;
use PVE::Cluster qw(cfs_register_file cfs_lock_file cfs_read_file cfs_write_file);
use PVE::DataCenterConfig;
use PVE::Exception qw(raise_param_exc);
use PVE::Firewall;
use PVE::HA::Config;
use PVE::HA::Env::PVE2;
use PVE::INotify;
use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::SafeSyslog;
use PVE::Storage;
use PVE::Tools qw(extract_param);
use PVE::API2::ACMEAccount;
use PVE::API2::ACMEPlugin;
use PVE::API2::Backup;
use PVE::API2::BackupInfo;
use PVE::API2::Cluster::Ceph;
use PVE::API2::Cluster::MetricServer;
use PVE::API2::ClusterConfig;
use PVE::API2::Firewall::Cluster;
use PVE::API2::HAConfig;
use PVE::API2::ReplicationConfig;
my $have_sdn;
eval {
require PVE::API2::Network::SDN;
$have_sdn = 1;
};
use base qw(PVE::RESTHandler);
__PACKAGE__->register_method ({
subclass => "PVE::API2::ReplicationConfig",
path => 'replication',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Cluster::MetricServer",
path => 'metrics',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::ClusterConfig",
path => 'config',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Firewall::Cluster",
path => 'firewall',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Backup",
path => 'backup',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::BackupInfo",
path => 'backupinfo',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::HAConfig",
path => 'ha',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::ACMEAccount",
path => 'acme',
});
__PACKAGE__->register_method ({
subclass => "PVE::API2::Cluster::Ceph",
path => 'ceph',
});
if ($have_sdn) {
__PACKAGE__->register_method ({
subclass => "PVE::API2::Network::SDN",
path => 'sdn',
});
}
my $dc_schema = PVE::DataCenterConfig::get_datacenter_schema();
my $dc_properties = {
delete => {
type => 'string', format => 'pve-configid-list',
description => "A list of settings you want to delete.",
optional => 1,
}
};
foreach my $opt (keys %{$dc_schema->{properties}}) {
$dc_properties->{$opt} = $dc_schema->{properties}->{$opt};
}
__PACKAGE__->register_method ({
name => 'index',
path => '',
method => 'GET',
description => "Cluster index.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
links => [ { rel => 'child', href => "{name}" } ],
},
code => sub {
my ($param) = @_;
my $result = [
{ name => 'log' },
{ name => 'options' },
{ name => 'resources' },
{ name => 'replication' },
{ name => 'tasks' },
{ name => 'backup' },
{ name => 'backupinfo' },
{ name => 'ha' },
{ name => 'status' },
{ name => 'nextid' },
{ name => 'firewall' },
{ name => 'config' },
{ name => 'acme' },
{ name => 'ceph' },
{ name => 'metrics' },
];
if ($have_sdn) {
push(@{$result}, { name => 'sdn' });
}
return $result;
}});
__PACKAGE__->register_method({
name => 'log',
path => 'log',
method => 'GET',
description => "Read cluster log",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
max => {
type => 'integer',
description => "Maximum number of entries.",
optional => 1,
minimum => 1,
}
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {},
},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $max = $param->{max} || 0;
my $user = $rpcenv->get_user();
my $admin = $rpcenv->check($user, "/", [ 'Sys.Syslog' ], 1);
my $loguser = $admin ? '' : $user;
my $res = decode_json(PVE::Cluster::get_cluster_log($loguser, $max));
foreach my $entry (@{$res->{data}}) {
$entry->{id} = "$entry->{uid}:$entry->{node}";
}
return $res->{data};
}});
__PACKAGE__->register_method({
name => 'resources',
path => 'resources',
method => 'GET',
description => "Resources index (cluster wide).",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
type => {
type => 'string',
optional => 1,
enum => ['vm', 'storage', 'node', 'sdn'],
},
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
id => { type => 'string' },
type => {
description => "Resource type.",
type => 'string',
enum => ['node', 'storage', 'pool', 'qemu', 'lxc', 'openvz', 'sdn'],
},
status => {
description => "Resource type dependent status.",
type => 'string',
optional => 1,
},
name => {
description => "Name of the resource.",
type => 'string',
optional => 1,
},
node => get_standard_option('pve-node', {
description => "The cluster node name (when type in node,storage,qemu,lxc).",
optional => 1,
}),
storage => get_standard_option('pve-storage-id', {
description => "The storage identifier (when type == storage).",
optional => 1,
}),
pool => {
description => "The pool name (when type in pool,qemu,lxc).",
type => 'string',
optional => 1,
},
cpu => {
description => "CPU utilization (when type in node,qemu,lxc).",
type => 'number',
optional => 1,
renderer => 'fraction_as_percentage',
},
maxcpu => {
description => "Number of available CPUs (when type in node,qemu,lxc).",
type => 'number',
optional => 1,
},
mem => {
description => "Used memory in bytes (when type in node,qemu,lxc).",
type => 'string',
optional => 1,
renderer => 'bytes',
},
maxmem => {
description => "Number of available memory in bytes (when type in node,qemu,lxc).",
type => 'integer',
optional => 1,
renderer => 'bytes',
},
level => {
description => "Support level (when type == node).",
type => 'string',
optional => 1,
},
uptime => {
description => "Node uptime in seconds (when type in node,qemu,lxc).",
type => 'integer',
optional => 1,
renderer => 'duration',
},
hastate => {
description => "HA service status (for HA managed VMs).",
type => 'string',
optional => 1,
},
disk => {
description => "Used disk space in bytes (when type in storage), used root image spave for VMs (type in qemu,lxc).",
type => 'string',
optional => 1,
renderer => 'bytes',
},
maxdisk => {
description => "Storage size in bytes (when type in storage), root image size for VMs (type in qemu,lxc).",
type => 'integer',
optional => 1,
renderer => 'bytes',
},
content => {
description => "Allowed storage content types (when type == storage).",
type => 'string',
format => 'pve-storage-content-list',
optional => 1,
},
plugintype => {
description => "More specific type, if available.",
type => 'string',
optional => 1,
},
},
},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $usercfg = $rpcenv->{user_cfg};
my $res = [];
my $nodelist = PVE::Cluster::get_nodelist();
my $members = PVE::Cluster::get_members();
my $rrd = PVE::Cluster::rrd_dump();
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
my $hastatus = PVE::HA::Config::read_manager_status();
my $haresources = PVE::HA::Config::read_resources_config();
my $hatypemap = {
'qemu' => 'vm',
'lxc' => 'ct'
};
my $pooldata = {};
if (!$param->{type} || $param->{type} eq 'pool') {
for my $pool (sort keys %{$usercfg->{pools}}) {
my $d = $usercfg->{pools}->{$pool};
next if !$rpcenv->check($authuser, "/pool/$pool", [ 'Pool.Allocate' ], 1);
my $entry = {
id => "/pool/$pool",
pool => $pool,
type => 'pool',
};
$pooldata->{$pool} = $entry;
push @$res, $entry;
}
}
# we try to generate 'numbers' by using "$X + 0"
if (!$param->{type} || $param->{type} eq 'vm') {
my $locked_vms = PVE::Cluster::get_guest_config_property('lock');
for my $vmid (sort keys %$idlist) {
my $data = $idlist->{$vmid};
my $entry = PVE::API2Tools::extract_vm_stats($vmid, $data, $rrd);
if (my $pool = $usercfg->{vms}->{$vmid}) {
$entry->{pool} = $pool;
if (my $pe = $pooldata->{$pool}) {
if ($entry->{uptime}) {
$pe->{uptime} = $entry->{uptime} if !$pe->{uptime} || $entry->{uptime} > $pe->{uptime};
$pe->{mem} = 0 if !$pe->{mem};
$pe->{mem} += $entry->{mem};
$pe->{maxmem} = 0 if !$pe->{maxmem};
$pe->{maxmem} += $entry->{maxmem};
$pe->{cpu} = 0 if !$pe->{cpu};
$pe->{maxcpu} = 0 if !$pe->{maxcpu};
# explanation:
# we do not know how much cpus there are in the cluster at this moment
# so we calculate the current % of the cpu
# but we had already the old cpu % before this vm, so:
# new% = (old%*oldmax + cur%*curmax) / (oldmax+curmax)
$pe->{cpu} = (($pe->{cpu} * $pe->{maxcpu}) + ($entry->{cpu} * $entry->{maxcpu})) / ($pe->{maxcpu} + $entry->{maxcpu});
$pe->{maxcpu} += $entry->{maxcpu};
}
}
}
# only skip now to next to ensure that the pool stats above are filled, if eligible
next if !$rpcenv->check($authuser, "/vms/$vmid", [ 'VM.Audit' ], 1);
if (defined(my $lock = $locked_vms->{$vmid}->{lock})) {
$entry->{lock} = $lock;
}
# get ha status
if (my $hatype = $hatypemap->{$entry->{type}}) {
my $sid = "$hatype:$vmid";
my $service;
if ($service = $hastatus->{service_status}->{$sid}) {
$entry->{hastate} = $service->{state};
} elsif ($service = $haresources->{ids}->{$sid}) {
$entry->{hastate} = $service->{state};
}
}
push @$res, $entry;
}
}
if (!$param->{type} || $param->{type} eq 'node') {
foreach my $node (@$nodelist) {
my $can_audit = $rpcenv->check($authuser, "/nodes/$node", [ 'Sys.Audit' ], 1);
my $entry = PVE::API2Tools::extract_node_stats($node, $members, $rrd, !$can_audit);
push @$res, $entry;
}
}
if (!$param->{type} || $param->{type} eq 'storage') {
my $cfg = PVE::Storage::config();
my @sids = PVE::Storage::storage_ids ($cfg);
foreach my $storeid (@sids) {
next if !$rpcenv->check($authuser, "/storage/$storeid", [ 'Datastore.Audit' ], 1);
my $scfg = PVE::Storage::storage_config($cfg, $storeid);
# we create a entry for each node
foreach my $node (@$nodelist) {
next if !PVE::Storage::storage_check_enabled($cfg, $storeid, $node, 1);
my $entry = PVE::API2Tools::extract_storage_stats($storeid, $scfg, $node, $rrd);
push @$res, $entry;
}
}
}
if ($have_sdn) {
if (!$param->{type} || $param->{type} eq 'sdn') {
my $nodes = PVE::Cluster::get_node_kv("sdn");
for my $node (sort keys %{$nodes}) {
my $sdns = decode_json($nodes->{$node});
for my $id (sort keys %{$sdns}) {
next if !$rpcenv->check($authuser, "/sdn/zones/$id", [ 'SDN.Audit' ], 1);
my $sdn = $sdns->{$id};
my $entry = {
id => "sdn/$node/$id",
sdn => $id,
node => $node,
type => 'sdn',
status => $sdn->{'status'},
};
push @$res, $entry;
}
}
}
}
return $res;
}});
__PACKAGE__->register_method({
name => 'tasks',
path => 'tasks',
method => 'GET',
description => "List recent tasks (cluster wide).",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
upid => { type => 'string' },
},
},
},
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $tlist = PVE::Cluster::get_tasklist();
my $res = [];
return $res if !$tlist;
my $all = $rpcenv->check($authuser, "/", [ 'Sys.Audit' ], 1);
foreach my $task (@$tlist) {
if (PVE::AccessControl::pve_verify_tokenid($task->{user}, 1)) {
($task->{user}, $task->{tokenid}) = PVE::AccessControl::split_tokenid($task->{user});
}
push @$res, $task if $all || ($task->{user} eq $authuser);
}
return $res;
}});
__PACKAGE__->register_method({
name => 'get_options',
path => 'options',
method => 'GET',
description => "Get datacenter options.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
},
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => "object",
properties => {},
},
code => sub {
my ($param) = @_;
return PVE::Cluster::cfs_read_file('datacenter.cfg');
}});
__PACKAGE__->register_method({
name => 'set_options',
path => 'options',
method => 'PUT',
description => "Set datacenter options.",
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => $dc_properties,
},
returns => { type => "null" },
code => sub {
my ($param) = @_;
my $filename = 'datacenter.cfg';
my $delete = extract_param($param, 'delete');
my $code = sub {
my $conf = cfs_read_file($filename);
foreach my $opt (keys %$param) {
$conf->{$opt} = $param->{$opt};
}
foreach my $opt (PVE::Tools::split_list($delete)) {
delete $conf->{$opt};
};
cfs_write_file($filename, $conf);
};
cfs_lock_file($filename, undef, $code);
die $@ if $@;
return undef;
}});
__PACKAGE__->register_method({
name => 'get_status',
path => 'status',
method => 'GET',
description => "Get cluster status information.",
permissions => {
check => ['perm', '/', [ 'Sys.Audit' ]],
},
protected => 1,
parameters => {
additionalProperties => 0,
properties => {},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
type => {
type => 'string',
enum => ['cluster', 'node'],
description => 'Indicates the type, either cluster or node. The type defines the object properties e.g. quorate available for type cluster.'
},
id => {
type => 'string',
},
name => {
type => 'string',
},
nodes => {
type => 'integer',
optional => 1,
description => '[cluster] Nodes count, including offline nodes.',
},
version => {
type => 'integer',
optional => 1,
description => '[cluster] Current version of the corosync configuration file.',
},
quorate => {
type => 'boolean',
optional => 1,
description => '[cluster] Indicates if there is a majority of nodes online to make decisions',
},
nodeid => {
type => 'integer',
optional => 1,
description => '[node] ID of the node from the corosync configuration.',
},
ip => {
type => 'string',
optional => 1,
description => '[node] IP of the resolved nodename.',
},
'local' => {
type => 'boolean',
optional => 1,
description => '[node] Indicates if this is the responding node.',
},
online => {
type => 'boolean',
optional => 1,
description => '[node] Indicates if the node is online or offline.',
},
level => {
type => 'string',
optional => 1,
description => '[node] Proxmox VE Subscription level, indicates if eligible for enterprise support as well as access to the stable Proxmox VE Enterprise Repository.',
}
},
},
},
code => sub {
my ($param) = @_;
# make sure we get current info
PVE::Cluster::cfs_update();
# we also add info from pmxcfs
my $clinfo = PVE::Cluster::get_clinfo();
my $members = PVE::Cluster::get_members();
my $nodename = PVE::INotify::nodename();
my $rrd = PVE::Cluster::rrd_dump();
if ($members) {
my $res = [];
if (my $d = $clinfo->{cluster}) {
push @$res, {
type => 'cluster',
id => 'cluster',
nodes => $d->{nodes},
version => $d->{version},
name => $d->{name},
quorate => $d->{quorate},
};
}
foreach my $node (keys %$members) {
my $d = $members->{$node};
my $entry = {
type => 'node',
id => "node/$node",
name => $node,
nodeid => $d->{id},
'local' => ($node eq $nodename) ? 1 : 0,
online => $d->{online},
};
if (defined($d->{ip})) {
$entry->{ip} = $d->{ip};
}
if (my $d = PVE::API2Tools::extract_node_stats($node, $members, $rrd)) {
$entry->{level} = $d->{level} || '';
}
push @$res, $entry;
}
return $res;
} else {
# fake entry for local node if no cluster defined
my $pmxcfs = ($clinfo && $clinfo->{version}) ? 1 : 0; # pmxcfs online ?
my $subinfo = PVE::INotify::read_file('subscription');
my $sublevel = $subinfo->{level} || '';
return [{
type => 'node',
id => "node/$nodename",
name => $nodename,
ip => scalar(PVE::Cluster::remote_node_ip($nodename)),
'local' => 1,
nodeid => 0,
online => 1,
level => $sublevel,
}];
}
}});
__PACKAGE__->register_method({
name => 'nextid',
path => 'nextid',
method => 'GET',
description => "Get next free VMID. If you pass an VMID it will raise an error if the ID is already used.",
permissions => { user => 'all' },
parameters => {
additionalProperties => 0,
properties => {
vmid => get_standard_option('pve-vmid', {optional => 1}),
},
},
returns => {
type => 'integer',
description => "The next free VMID.",
},
code => sub {
my ($param) = @_;
my $vmlist = PVE::Cluster::get_vmlist() || {};
my $idlist = $vmlist->{ids} || {};
if (my $vmid = $param->{vmid}) {
return $vmid if !defined($idlist->{$vmid});
raise_param_exc({ vmid => "VM $vmid already exists" });
}
for (my $i = 100; $i < 10000; $i++) {
return $i if !defined($idlist->{$i});
}
die "unable to get any free VMID\n";
}});
1;