]>
Commit | Line | Data |
---|---|---|
58541b94 AD |
1 | package PVE::Status::InfluxDB; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
fa6f3716 | 5 | |
8077d94a DC |
6 | use POSIX qw(isnan isinf); |
7 | use Scalar::Util 'looks_like_number'; | |
5c77a34f | 8 | use IO::Socket::IP; |
ccb61431 DC |
9 | use LWP::UserAgent; |
10 | use HTTP::Request; | |
58541b94 | 11 | |
fa6f3716 TL |
12 | use PVE::SafeSyslog; |
13 | ||
14 | use PVE::Status::Plugin; | |
15 | ||
58541b94 AD |
16 | use base('PVE::Status::Plugin'); |
17 | ||
18 | sub type { | |
19 | return 'influxdb'; | |
20 | } | |
21 | ||
ccb61431 DC |
22 | sub 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 |
67 | sub 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 |
84 | my $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 |
99 | sub 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 | ||
107 | sub 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 | ||
122 | sub 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 | ||
137 | sub 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 | 151 | sub _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 | ||
161 | sub 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 | ||
185 | sub _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 |
195 | sub _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 |
210 | sub _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 |
244 | sub 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 | 276 | sub 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 |
317 | sub 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 | ||
336 | sub 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 |
357 | my $priv_dir = "/etc/pve/priv/metricserver"; |
358 | ||
359 | sub cred_file_name { | |
360 | my ($id) = @_; | |
361 | return "${priv_dir}/${id}.pw"; | |
362 | } | |
363 | ||
364 | sub 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 | ||
375 | sub 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 | ||
385 | sub 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 | ||
396 | sub 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 | ||
410 | sub 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 | ||
424 | sub on_delete_hook { | |
425 | my ($class, $id, $opts) = @_; | |
426 | ||
427 | delete_credentials($id); | |
428 | ||
429 | return undef; | |
430 | } | |
431 | ||
432 | ||
58541b94 | 433 | 1; |