1 package PVE
::Status
::InfluxDB
;
6 use POSIX
qw(isnan isinf);
7 use Scalar
::Util
'looks_like_number';
14 use PVE
::Status
::Plugin
;
16 use base
('PVE::Status::Plugin');
25 description
=> "The InfluxDB organization. Only necessary when using the http v2 api."
26 ." Has no meaning when using v2 compatibility api.",
31 description
=> "The InfluxDB bucket/db. Only necessary when using the http v2 api.",
36 description
=> "The InfluxDB access token. Only necessary when using the http v2 api."
37 ." If the v2 compatibility api is used, use 'user:password' instead.",
41 'api-path-prefix' => {
42 description
=> "An API path prefix inserted between '<host>:<port>/' and '/api2/'."
43 ." Can be useful if the InfluxDB service runs behind a reverse proxy.",
49 enum
=> ['udp', 'http', 'https'],
54 description
=> "InfluxDB max-body-size in bytes. Requests are batched up to this size.",
57 default => 25_000_000,
59 'verify-certificate' => {
60 description
=> "Set to 0 to disable certificate verification for https endpoints.",
71 mtu
=> { optional
=> 1 },
72 disable
=> { optional
=> 1 },
73 organization
=> { optional
=> 1},
74 bucket
=> { optional
=> 1},
75 token
=> { optional
=> 1},
76 influxdbproto
=> { optional
=> 1},
77 timeout
=> { optional
=> 1},
78 'max-body-size' => { optional
=> 1 },
79 'api-path-prefix' => { optional
=> 1 },
80 'verify-certificate' => { optional
=> 1 },
84 my $set_ssl_opts = sub {
87 my $cert_verify = $cfg->{'verify-certificate'} // 1;
91 SSL_verify_mode
=> IO
::Socket
::SSL
::SSL_VERIFY_NONE
,
98 # Plugin implementation
99 sub update_node_status
{
100 my ($class, $txn, $node, $data, $ctime) = @_;
102 $ctime *= 1000000000;
104 build_influxdb_payload
($class, $txn, $data, $ctime, "object=nodes,host=$node");
107 sub update_qemu_status
{
108 my ($class, $txn, $vmid, $data, $ctime, $nodename) = @_;
110 $ctime *= 1000000000;
112 my $object = "object=qemu,vmid=$vmid,nodename=$nodename";
113 if($data->{name
} && $data->{name
} ne '') {
114 $object .= ",host=$data->{name}";
116 $object =~ s/\s/\\ /g;
118 # VMID is already added in base $object above, so exclude it from being re-added
119 build_influxdb_payload
($class, $txn, $data, $ctime, $object, { 'vmid' => 1 });
122 sub update_lxc_status
{
123 my ($class, $txn, $vmid, $data, $ctime, $nodename) = @_;
125 $ctime *= 1000000000;
127 my $object = "object=lxc,vmid=$vmid,nodename=$nodename";
128 if($data->{name
} && $data->{name
} ne '') {
129 $object .= ",host=$data->{name}";
131 $object =~ s/\s/\\ /g;
133 # VMID is already added in base $object above, so exclude it from being re-added
134 build_influxdb_payload
($class, $txn, $data, $ctime, $object, { 'vmid' => 1 });
137 sub update_storage_status
{
138 my ($class, $txn, $nodename, $storeid, $data, $ctime) = @_;
140 $ctime *= 1000000000;
142 my $object = "object=storages,nodename=$nodename,host=$storeid";
143 if($data->{type
} && $data->{type
} ne '') {
144 $object .= ",type=$data->{type}";
146 $object =~ s/\s/\\ /g;
148 build_influxdb_payload
($class, $txn, $data, $ctime, $object);
151 sub _send_batch_size
{
152 my ($class, $cfg) = @_;
153 my $proto = $cfg->{influxdbproto
} // 'udp';
154 if ($proto ne 'udp') {
155 return $cfg->{'max-body-size'} // 25_000_000;
158 return $class->SUPER::_send_batch_size
($cfg);
162 my ($class, $connection, $data, $cfg) = @_;
164 my $proto = $cfg->{influxdbproto
} // 'udp';
165 if ($proto eq 'udp') {
166 return $class->SUPER::send($connection, $data, $cfg);
167 } elsif ($proto =~ m/^https?$/) {
168 my $ua = LWP
::UserAgent-
>new();
169 $set_ssl_opts->($cfg, $ua);
170 $ua->timeout($cfg->{timeout
} // 1);
171 $connection->content($data);
172 my $response = $ua->request($connection);
174 if (!$response->is_success) {
175 my $err = $response->status_line;
179 die "invalid protocol\n";
186 my ($class, $connection, $cfg) = @_;
187 my $proto = $cfg->{influxdbproto
} // 'udp';
188 if ($proto eq 'udp') {
189 return $class->SUPER::_disconnect
($connection, $cfg);
196 my ($cfg, $api_path) = @_;
197 my ($proto, $host, $port) = $cfg->@{qw(influxdbproto server port)};
198 my $api_prefix = $cfg->{'api-path-prefix'} // '/';
199 if ($api_prefix ne '/' && $api_prefix =~ m
!^/*(.+)/*$!) {
200 $api_prefix = "/$1/";
203 if ($api_path ne 'health') {
204 $api_path = "api/v2/${api_path}";
207 return "${proto}://${host}:${port}${api_prefix}${api_path}";
211 my ($class, $cfg, $id) = @_;
213 my $host = $cfg->{server
};
214 my $port = $cfg->{port
};
215 my $proto = $cfg->{influxdbproto
} // 'udp';
217 if ($proto eq 'udp') {
218 my $socket = IO
::Socket
::IP-
>new(
222 ) || die "couldn't create influxdb socket [$host]:$port - $@\n";
224 $socket->blocking(0);
227 } elsif ($proto =~ m/^https?$/) {
228 my $token = get_credentials
($id, 1);
229 my $org = $cfg->{organization
} // 'proxmox';
230 my $bucket = $cfg->{bucket
} // 'proxmox';
231 my $url = _get_v2url
($cfg, "write?org=${org}&bucket=${bucket}");
233 my $req = HTTP
::Request-
>new(POST
=> $url);
234 if (defined($token)) {
235 $req->header( "Authorization", "Token $token");
241 die "cannot connect to InfluxDB: invalid protocol '$proto'\n";
244 sub test_connection
{
245 my ($class, $cfg, $id) = @_;
247 # do not check connection for disabled plugins
248 return if $cfg->{disable
};
250 my $proto = $cfg->{influxdbproto
} // 'udp';
251 if ($proto eq 'udp') {
252 return $class->SUPER::test_connection
($cfg, $id);
253 } elsif ($proto =~ m/^https?$/) {
254 my $url = _get_v2url
($cfg, "health");
255 my $ua = LWP
::UserAgent-
>new();
256 $set_ssl_opts->($cfg, $ua);
257 $ua->timeout($cfg->{timeout
} // 1);
258 # in the initial add connection test, the token may still be in $cfg
259 my $token = $cfg->{token
} // get_credentials
($id, 1);
260 if (defined($token)) {
261 $ua->default_header("Authorization" => "Token $token");
263 my $response = $ua->get($url);
265 if (!$response->is_success) {
266 my $err = $response->status_line;
270 die "invalid protocol\n";
276 sub build_influxdb_payload
{
277 my ($class, $txn, $data, $ctime, $tags, $excluded, $measurement, $instance) = @_;
279 # 'abc' and '123' are both valid hostnames, that confuses influx's type detection
280 my $to_quote = { name
=> 1 };
284 foreach my $key (sort keys %$data) {
285 next if defined($excluded) && $excluded->{$key};
286 my $value = $data->{$key};
287 next if !defined($value);
289 if (!ref($value) && $value ne '') {
292 if (defined(my $v = prepare_value
($value, $to_quote->{$key}))) {
293 push @values, "$key=$v";
295 } elsif (ref($value) eq 'HASH') {
298 if (!defined($measurement)) {
299 build_influxdb_payload
($class, $txn, $value, $ctime, $tags, $excluded, $key);
300 } elsif(!defined($instance)) {
301 build_influxdb_payload
($class, $txn, $value, $ctime, $tags, $excluded, $measurement, $key);
303 push @values, get_recursive_values
($value);
309 my $mm = $measurement // 'system';
310 my $tagstring = $tags;
311 $tagstring .= ",instance=$instance" if defined($instance);
312 my $valuestr = join(',', @values);
313 $class->add_metric_data($txn, "$mm,$tagstring $valuestr $ctime\n");
317 sub get_recursive_values
{
322 foreach my $key (keys %$hash) {
323 my $value = $hash->{$key};
324 if(ref($value) eq 'HASH') {
325 push(@values, get_recursive_values
($value));
326 } elsif (!ref($value) && $value ne '') {
327 if (defined(my $v = prepare_value
($value))) {
328 push @values, "$key=$v";
337 my ($value, $force_quote) = @_;
339 # don't treat value like a number if quote is 1
340 if (!$force_quote && looks_like_number
($value)) {
341 if (isnan
($value) || isinf
($value)) {
342 # we cannot send influxdb NaN or Inf
346 # influxdb also accepts 1.0e+10, etc.
350 # non-numeric values require to be quoted, so escape " with \"
351 $value =~ s/\"/\\\"/g;
352 $value = "\"$value\"";
357 my $priv_dir = "/etc/pve
/priv/metricserver
";
361 return "${priv_dir
}/${id
}.pw
";
364 sub delete_credentials {
367 if (my $cred_file = cred_file_name($id)) {
369 or warn "removing influxdb credentials file
'$cred_file' failed
: $!\n";
375 sub set_credentials {
376 my ($id, $token) = @_;
378 my $cred_file = cred_file_name($id);
382 PVE::Tools::file_set_contents($cred_file, "$token");
385 sub get_credentials {
386 my ($id, $silent) = @_;
388 my $cred_file = cred_file_name($id);
390 my $creds = eval { PVE::Tools::file_get_contents($cred_file) };
391 warn "could
not load credentials
for '$id': $@\n" if $@ && !$silent;
397 my ($class, $id, $opts, $sensitive_opts) = @_;
399 my $token = $sensitive_opts->{token};
401 if (defined($token)) {
402 set_credentials($id, $token);
404 delete_credentials($id);
411 my ($class, $id, $opts, $sensitive_opts) = @_;
412 return if !exists($sensitive_opts->{token});
414 my $token = $sensitive_opts->{token};
415 if (defined($token)) {
416 set_credentials($id, $token);
418 delete_credentials($id);
425 my ($class, $id, $opts) = @_;
427 delete_credentials($id);