]> git.proxmox.com Git - pve-manager.git/blame - PVE/Status/InfluxDB.pm
api2: network: improve code readability
[pve-manager.git] / PVE / Status / InfluxDB.pm
CommitLineData
58541b94
AD
1package PVE::Status::InfluxDB;
2
3use strict;
4use warnings;
fa6f3716 5
8077d94a
DC
6use POSIX qw(isnan isinf);
7use Scalar::Util 'looks_like_number';
5c77a34f 8use IO::Socket::IP;
ccb61431
DC
9use LWP::UserAgent;
10use HTTP::Request;
58541b94 11
fa6f3716
TL
12use PVE::SafeSyslog;
13
14use PVE::Status::Plugin;
15
58541b94
AD
16use base('PVE::Status::Plugin');
17
18sub type {
19 return 'influxdb';
20}
21
ccb61431
DC
22sub properties {
23 return {
24 organization => {
f8d1d5ad
TL
25 description => "The InfluxDB organization. Only necessary when using the http v2 api."
26 ." Has no meaning when using v2 compatibility api.",
ccb61431
DC
27 type => 'string',
28 optional => 1,
29 },
30 bucket => {
f8d1d5ad 31 description => "The InfluxDB bucket/db. Only necessary when using the http v2 api.",
ccb61431
DC
32 type => 'string',
33 optional => 1,
34 },
35 token => {
f8d1d5ad
TL
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.",
ccb61431
DC
38 type => 'string',
39 optional => 1,
40 },
23c9eaf6
TL
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.",
44 type => 'string',
45 optional => 1,
46 },
ccb61431
DC
47 influxdbproto => {
48 type => 'string',
49 enum => ['udp', 'http', 'https'],
50 default => 'udp',
51 optional => 1,
52 },
53 'max-body-size' => {
f8d1d5ad 54 description => "InfluxDB max-body-size in bytes. Requests are batched up to this size.",
ccb61431
DC
55 type => 'integer',
56 minimum => 1,
57 default => 25_000_000,
dcdbc232
DC
58 },
59 'verify-certificate' => {
60 description => "Set to 0 to disable certificate verification for https endpoints.",
61 type => 'boolean',
62 optional => 1,
63 default => 1,
64 },
ccb61431
DC
65 };
66}
58541b94
AD
67sub options {
68 return {
69 server => {},
70 port => {},
0fc553eb 71 mtu => { optional => 1 },
58541b94 72 disable => { optional => 1 },
ccb61431
DC
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 },
23c9eaf6 79 'api-path-prefix' => { optional => 1 },
dcdbc232 80 'verify-certificate' => { optional => 1 },
58541b94
AD
81 };
82}
83
dcdbc232
DC
84my $set_ssl_opts = sub {
85 my ($cfg, $ua) = @_;
86
87 my $cert_verify = $cfg->{'verify-certificate'} // 1;
88 if (!$cert_verify) {
89 $ua->ssl_opts(
90 verify_hostname => 0,
91 SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE,
92 );
93 }
94
95 return;
96};
97
58541b94
AD
98# Plugin implementation
99sub update_node_status {
87be2c19 100 my ($class, $txn, $node, $data, $ctime) = @_;
58541b94
AD
101
102 $ctime *= 1000000000;
103
5c77a34f 104 build_influxdb_payload($class, $txn, $data, $ctime, "object=nodes,host=$node");
58541b94
AD
105}
106
107sub update_qemu_status {
87be2c19 108 my ($class, $txn, $vmid, $data, $ctime, $nodename) = @_;
58541b94
AD
109
110 $ctime *= 1000000000;
111
09f19204 112 my $object = "object=qemu,vmid=$vmid,nodename=$nodename";
58541b94
AD
113 if($data->{name} && $data->{name} ne '') {
114 $object .= ",host=$data->{name}";
115 }
116 $object =~ s/\s/\\ /g;
87be2c19 117
5d551f5e 118 # VMID is already added in base $object above, so exclude it from being re-added
456be471 119 build_influxdb_payload($class, $txn, $data, $ctime, $object, { 'vmid' => 1 });
58541b94
AD
120}
121
122sub update_lxc_status {
87be2c19 123 my ($class, $txn, $vmid, $data, $ctime, $nodename) = @_;
58541b94
AD
124
125 $ctime *= 1000000000;
126
09f19204 127 my $object = "object=lxc,vmid=$vmid,nodename=$nodename";
58541b94
AD
128 if($data->{name} && $data->{name} ne '') {
129 $object .= ",host=$data->{name}";
130 }
131 $object =~ s/\s/\\ /g;
132
5d551f5e 133 # VMID is already added in base $object above, so exclude it from being re-added
456be471 134 build_influxdb_payload($class, $txn, $data, $ctime, $object, { 'vmid' => 1 });
58541b94
AD
135}
136
137sub update_storage_status {
87be2c19 138 my ($class, $txn, $nodename, $storeid, $data, $ctime) = @_;
58541b94
AD
139
140 $ctime *= 1000000000;
141
142 my $object = "object=storages,nodename=$nodename,host=$storeid";
143 if($data->{type} && $data->{type} ne '') {
144 $object .= ",type=$data->{type}";
145 }
146 $object =~ s/\s/\\ /g;
147
5c77a34f 148 build_influxdb_payload($class, $txn, $data, $ctime, $object);
58541b94
AD
149}
150
ccb61431 151sub _send_batch_size {
68f58b5d 152 my ($class, $cfg) = @_;
ccb61431
DC
153 my $proto = $cfg->{influxdbproto} // 'udp';
154 if ($proto ne 'udp') {
155 return $cfg->{'max-body-size'} // 25_000_000;
156 }
157
158 return $class->SUPER::_send_batch_size($cfg);
159}
160
161sub send {
162 my ($class, $connection, $data, $cfg) = @_;
163
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();
dcdbc232 169 $set_ssl_opts->($cfg, $ua);
ccb61431
DC
170 $ua->timeout($cfg->{timeout} // 1);
171 $connection->content($data);
172 my $response = $ua->request($connection);
173
174 if (!$response->is_success) {
175 my $err = $response->status_line;
176 die "$err\n";
177 }
178 } else {
179 die "invalid protocol\n";
180 }
181
182 return;
183}
184
185sub _disconnect {
186 my ($class, $connection, $cfg) = @_;
187 my $proto = $cfg->{influxdbproto} // 'udp';
188 if ($proto eq 'udp') {
189 return $class->SUPER::_disconnect($connection, $cfg);
190 }
191
192 return;
193}
194
23c9eaf6
TL
195sub _get_v2url {
196 my ($cfg, $api_path) = @_;
197 my ($proto, $host, $port) = $cfg->@{qw(influxdbproto server port)};
198 my $api_prefix = $cfg->{'api-path-prefix'} // '/';
bc33c739 199 if ($api_prefix ne '/' && $api_prefix =~ m!^/*(.+)/*$!) {
23c9eaf6
TL
200 $api_prefix = "/$1/";
201 }
c7777408
DC
202
203 if ($api_path ne 'health') {
204 $api_path = "api/v2/${api_path}";
205 }
206
207 return "${proto}://${host}:${port}${api_prefix}${api_path}";
23c9eaf6
TL
208}
209
ccb61431
DC
210sub _connect {
211 my ($class, $cfg, $id) = @_;
58541b94 212
68f58b5d
TL
213 my $host = $cfg->{server};
214 my $port = $cfg->{port};
ccb61431
DC
215 my $proto = $cfg->{influxdbproto} // 'udp';
216
217 if ($proto eq 'udp') {
218 my $socket = IO::Socket::IP->new(
219 PeerAddr => $host,
220 PeerPort => $port,
221 Proto => 'udp',
222 ) || die "couldn't create influxdb socket [$host]:$port - $@\n";
223
224 $socket->blocking(0);
225
226 return $socket;
227 } elsif ($proto =~ m/^https?$/) {
cf2063d4 228 my $token = get_credentials($id, 1);
ccb61431
DC
229 my $org = $cfg->{organization} // 'proxmox';
230 my $bucket = $cfg->{bucket} // 'proxmox';
23c9eaf6 231 my $url = _get_v2url($cfg, "write?org=${org}&bucket=${bucket}");
ccb61431
DC
232
233 my $req = HTTP::Request->new(POST => $url);
234 if (defined($token)) {
235 $req->header( "Authorization", "Token $token");
236 }
58541b94 237
ccb61431
DC
238 return $req;
239 }
240
9f8d8f2b 241 die "cannot connect to InfluxDB: invalid protocol '$proto'\n";
ccb61431 242}
58541b94 243
ccb61431
DC
244sub test_connection {
245 my ($class, $cfg, $id) = @_;
246
6ccef1da
DC
247 # do not check connection for disabled plugins
248 return if $cfg->{disable};
249
ccb61431
DC
250 my $proto = $cfg->{influxdbproto} // 'udp';
251 if ($proto eq 'udp') {
252 return $class->SUPER::test_connection($cfg, $id);
253 } elsif ($proto =~ m/^https?$/) {
23c9eaf6 254 my $url = _get_v2url($cfg, "health");
ccb61431 255 my $ua = LWP::UserAgent->new();
dcdbc232 256 $set_ssl_opts->($cfg, $ua);
ccb61431 257 $ua->timeout($cfg->{timeout} // 1);
bb35a833 258 # in the initial add connection test, the token may still be in $cfg
6e5405fb 259 my $token = $cfg->{token} // get_credentials($id, 1);
bb35a833
TL
260 if (defined($token)) {
261 $ua->default_header("Authorization" => "Token $token");
262 }
ccb61431
DC
263 my $response = $ua->get($url);
264
265 if (!$response->is_success) {
266 my $err = $response->status_line;
267 die "$err\n";
268 }
269 } else {
270 die "invalid protocol\n";
271 }
5c77a34f 272
ccb61431 273 return;
68f58b5d
TL
274}
275
58541b94 276sub build_influxdb_payload {
456be471 277 my ($class, $txn, $data, $ctime, $tags, $excluded, $measurement, $instance) = @_;
58541b94 278
9d37535b
MF
279 # 'abc' and '123' are both valid hostnames, that confuses influx's type detection
280 my $to_quote = { name => 1 };
84502c7d 281
988b5a26 282 my @values = ();
58541b94 283
7d7f77ba 284 foreach my $key (sort keys %$data) {
456be471 285 next if defined($excluded) && $excluded->{$key};
7d7f77ba
DC
286 my $value = $data->{$key};
287 next if !defined($value);
288
289 if (!ref($value) && $value ne '') {
290 # value is scalar
291
9d37535b 292 if (defined(my $v = prepare_value($value, $to_quote->{$key}))) {
8077d94a
DC
293 push @values, "$key=$v";
294 }
7d7f77ba
DC
295 } elsif (ref($value) eq 'HASH') {
296 # value is a hash
297
298 if (!defined($measurement)) {
456be471 299 build_influxdb_payload($class, $txn, $value, $ctime, $tags, $excluded, $key);
7d7f77ba 300 } elsif(!defined($instance)) {
456be471 301 build_influxdb_payload($class, $txn, $value, $ctime, $tags, $excluded, $measurement, $key);
7d7f77ba
DC
302 } else {
303 push @values, get_recursive_values($value);
304 }
305 }
306 }
58541b94 307
7d7f77ba
DC
308 if (@values > 0) {
309 my $mm = $measurement // 'system';
310 my $tagstring = $tags;
311 $tagstring .= ",instance=$instance" if defined($instance);
5c77a34f
TL
312 my $valuestr = join(',', @values);
313 $class->add_metric_data($txn, "$mm,$tagstring $valuestr $ctime\n");
7d7f77ba
DC
314 }
315}
58541b94 316
7d7f77ba
DC
317sub get_recursive_values {
318 my ($hash) = @_;
58541b94 319
7d7f77ba 320 my @values = ();
58541b94 321
7d7f77ba
DC
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 '') {
8077d94a
DC
327 if (defined(my $v = prepare_value($value))) {
328 push @values, "$key=$v";
329 }
7d7f77ba 330 }
58541b94 331 }
988b5a26 332
7d7f77ba
DC
333 return @values;
334}
335
336sub prepare_value {
84502c7d 337 my ($value, $force_quote) = @_;
7d7f77ba 338
9d37535b 339 # don't treat value like a number if quote is 1
84502c7d 340 if (!$force_quote && looks_like_number($value)) {
8077d94a
DC
341 if (isnan($value) || isinf($value)) {
342 # we cannot send influxdb NaN or Inf
343 return undef;
344 }
345
346 # influxdb also accepts 1.0e+10, etc.
347 return $value;
348 }
349
2c4bf90f 350 # non-numeric values require to be quoted, so escape " with \"
8077d94a
DC
351 $value =~ s/\"/\\\"/g;
352 $value = "\"$value\"";
7d7f77ba
DC
353
354 return $value;
58541b94
AD
355}
356
ccb61431
DC
357my $priv_dir = "/etc/pve/priv/metricserver";
358
359sub cred_file_name {
360 my ($id) = @_;
361 return "${priv_dir}/${id}.pw";
362}
363
364sub delete_credentials {
365 my ($id) = @_;
366
367 if (my $cred_file = cred_file_name($id)) {
368 unlink($cred_file)
369 or warn "removing influxdb credentials file '$cred_file' failed: $!\n";
370 }
371
372 return;
373}
374
375sub set_credentials {
376 my ($id, $token) = @_;
377
378 my $cred_file = cred_file_name($id);
379
380 mkdir $priv_dir;
381
382 PVE::Tools::file_set_contents($cred_file, "$token");
383}
384
385sub get_credentials {
6e5405fb 386 my ($id, $silent) = @_;
ccb61431
DC
387
388 my $cred_file = cred_file_name($id);
389
6e5405fb
TL
390 my $creds = eval { PVE::Tools::file_get_contents($cred_file) };
391 warn "could not load credentials for '$id': $@\n" if $@ && !$silent;
392
393 return $creds;
ccb61431
DC
394}
395
396sub on_add_hook {
397 my ($class, $id, $opts, $sensitive_opts) = @_;
398
399 my $token = $sensitive_opts->{token};
400
401 if (defined($token)) {
402 set_credentials($id, $token);
403 } else {
c2162150 404 delete_credentials($id);
ccb61431
DC
405 }
406
407 return undef;
408}
409
410sub on_update_hook {
411 my ($class, $id, $opts, $sensitive_opts) = @_;
412 return if !exists($sensitive_opts->{token});
413
414 my $token = $sensitive_opts->{token};
415 if (defined($token)) {
416 set_credentials($id, $token);
417 } else {
c2162150 418 delete_credentials($id);
ccb61431
DC
419 }
420
421 return undef;
422}
423
424sub on_delete_hook {
425 my ($class, $id, $opts) = @_;
426
427 delete_credentials($id);
428
429 return undef;
430}
431
432
58541b94 4331;