package PVE::API2::Cluster::MetricServer; use warnings; use strict; use PVE::Tools qw(extract_param extract_sensitive_params); use PVE::Exception qw(raise_perm_exc raise_param_exc); use PVE::JSONSchema qw(get_standard_option); use PVE::INotify; use PVE::RPCEnvironment; use PVE::ExtMetric; use PVE::PullMetric; use PVE::SafeSyslog; use PVE::RESTHandler; use base qw(PVE::RESTHandler); __PACKAGE__->register_method ({ name => 'index', path => '', method => 'GET', description => "Metrics 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 => 'server' }, ]; return $result; } }); __PACKAGE__->register_method ({ name => 'server_index', path => 'server', method => 'GET', description => "List configured metric servers.", permissions => { check => ['perm', '/', ['Sys.Audit']], }, parameters => { 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}" } ], }, code => sub { my ($param) = @_; 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}, }; } return $res; }}); __PACKAGE__->register_method ({ name => 'read', path => 'server/{id}', method => 'GET', description => "Read metric server configuration.", permissions => { check => ['perm', '/', ['Sys.Audit']], }, parameters => { additionalProperties => 0, properties => { id => { type => 'string', format => 'pve-configid', }, }, }, returns => { type => 'object' }, code => sub { my ($param) = @_; 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"; } return $status_cfg->{ids}->{$id}; }}); __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']], }, parameters => PVE::Status::Plugin->createSchema(), returns => { type => 'null' }, code => sub { my ($param) = @_; 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'], []); 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}; my $opts = $plugin->check_config($id, $param, 1, 1); $cfg->{ids}->{$id} = $opts; $plugin->on_add_hook($id, $opts, $sensitive_params); eval { $plugin->test_connection($opts, $id); }; 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 $@; return; }}); __PACKAGE__->register_method ({ name => 'update', protected => 1, path => 'server/{id}', method => 'PUT', description => "Update metric server configuration.", permissions => { check => ['perm', '/', ['Sys.Modify']], }, parameters => PVE::Status::Plugin->updateSchema(), returns => { type => 'null' }, code => sub { my ($param) = @_; 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)]; } 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::SectionConfig::assert_if_modified($cfg, $digest); 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); 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}); delete $data->{$k}; } } $plugin->on_update_hook($id, $data, $sensitive_params); $plugin->test_connection($data, $id); PVE::Cluster::cfs_write_file('status.cfg', $cfg); }); die $@ if $@; return; }}); __PACKAGE__->register_method ({ name => 'delete', protected => 1, path => 'server/{id}', method => 'DELETE', description => "Remove Metric server.", permissions => { check => ['perm', '/', ['Sys.Modify']], }, parameters => { additionalProperties => 0, properties => { id => { type => 'string', format => 'pve-configid', }, } }, returns => { type => 'null' }, code => sub { my ($param) = @_; PVE::Cluster::cfs_lock_file('status.cfg', undef, sub { my $cfg = PVE::Cluster::cfs_read_file('status.cfg'); my $id = $param->{id}; my $plugin_cfg = $cfg->{ids}->{$id}; my $plugin = PVE::Status::Plugin->lookup($plugin_cfg->{type}); $plugin->on_delete_hook($id, $plugin_cfg); delete $cfg->{ids}->{$id}; PVE::Cluster::cfs_write_file('status.cfg', $cfg); }); die $@ if $@; return; }}); __PACKAGE__->register_method ({ name => 'export', path => 'export', method => 'GET', protected => 1, description => "Retrieve metrics of the cluster.", permissions => { 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, }, }, }, 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/' or" . " 'qemu/'." }, 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 $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(); } } else { $generations = 0; }; my @metrics = @{PVE::PullMetric::get_local_metrics($generations)}; if (defined($start)) { @metrics = grep { $_->{timestamp} > ($start) } @metrics; } my $nodename = PVE::INotify::nodename(); # 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 ($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); } 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; } 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, }; $conn_args->{cached_fingerprints} = { $fingerprint => 1 }; 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); $api_client->get('/cluster/metrics/export', $params); }; if ($@) { syslog('warning', "could not fetch metrics from $name: $@"); } else { push @metrics, $status->{data}->@*; } } } my @sorted = sort {$a->{timestamp} <=> $b->{timestamp}} @metrics; return { data => \@sorted, }; }, }); 1;