]> git.proxmox.com Git - pmg-api.git/blame - PMG/Utils.pm
PMG/AccessControl.pm: use userid including realm everywhere
[pmg-api.git] / PMG / Utils.pm
CommitLineData
758c7b6b
DM
1package PMG::Utils;
2
3use strict;
4use warnings;
758c7b6b 5use DBI;
b8ea5d5d 6use Net::Cmd;
758c7b6b 7use Net::SMTP;
26357b0a 8use IO::File;
cad3d400 9use File::stat;
d17c5265
DM
10use POSIX qw(strftime);
11use File::stat;
ff1c5a81 12use File::Basename;
758c7b6b
DM
13use MIME::Words;
14use MIME::Parser;
8210c7fa 15use Time::HiRes qw (gettimeofday);
26357b0a 16use Xdgmime;
d0d91cda 17use Data::Dumper;
62ebb4bc 18use Digest::SHA;
f609bf7f 19use Net::IP;
9f67f5b3 20use Socket;
dff363d9
DM
21use RRDs;
22use Filesys::Df;
758c7b6b 23
dff363d9 24use PVE::ProcFSTools;
f609bf7f 25use PVE::Network;
5953119e 26use PVE::Tools;
758c7b6b 27use PVE::SafeSyslog;
f609bf7f 28use PVE::ProcFSTools;
d0d91cda 29use PMG::AtomicFile;
8210c7fa 30use PMG::MailQueue;
c4df1b85 31use PMG::SMTPPrinter;
758c7b6b 32
62ebb4bc
DM
33my $realm_regex = qr/[A-Za-z][A-Za-z0-9\.\-_]+/;
34
35PVE::JSONSchema::register_format('pmg-realm', \&verify_realm);
36sub verify_realm {
37 my ($realm, $noerr) = @_;
38
39 if ($realm !~ m/^${realm_regex}$/) {
40 return undef if $noerr;
41 die "value does not look like a valid realm\n";
42 }
43 return $realm;
44}
45
46PVE::JSONSchema::register_standard_option('realm', {
47 description => "Authentication domain ID",
48 type => 'string', format => 'pmg-realm',
49 maxLength => 32,
50});
51
52PVE::JSONSchema::register_format('pmg-userid', \&verify_username);
53sub verify_username {
54 my ($username, $noerr) = @_;
55
56 $username = '' if !$username;
57 my $len = length($username);
58 if ($len < 3) {
59 die "user name '$username' is too short\n" if !$noerr;
60 return undef;
61 }
62 if ($len > 64) {
63 die "user name '$username' is too long ($len > 64)\n" if !$noerr;
64 return undef;
65 }
66
67 # we only allow a limited set of characters
68 # colon is not allowed, because we store usernames in
69 # colon separated lists)!
70 # slash is not allowed because it is used as pve API delimiter
71 # also see "man useradd"
72 if ($username =~ m!^([^\s:/]+)\@(${realm_regex})$!) {
73 return wantarray ? ($username, $1, $2) : $username;
74 }
75
76 die "value '$username' does not look like a valid user name\n" if !$noerr;
77
78 return undef;
79}
80
81PVE::JSONSchema::register_standard_option('userid', {
82 description => "User ID",
83 type => 'string', format => 'pmg-userid',
6b0180c3 84 minLength => 4,
62ebb4bc 85 maxLength => 64,
6b0180c3 86});
62ebb4bc
DM
87
88PVE::JSONSchema::register_standard_option('username', {
89 description => "Username (without realm)",
90 type => 'string',
91 pattern => '[^\s:\/\@]{3,60}',
6b0180c3 92 minLength => 4,
62ebb4bc
DM
93 maxLength => 64,
94});
95
c881fe35
DM
96sub msgquote {
97 my $msg = shift || '';
98 $msg =~ s/%/%%/g;
99 return $msg;
100}
101
758c7b6b
DM
102sub lastid {
103 my ($dbh, $seq) = @_;
104
105 return $dbh->last_insert_id(
106 undef, undef, undef, undef, { sequence => $seq});
107}
108
62ebb4bc
DM
109sub encrypt_pw {
110 my ($pw) = @_;
111
112 my $time = substr(Digest::SHA::sha1_base64(time), 0, 8);
113 return crypt(encode("utf8", $pw), "\$5\$$time\$");
114}
115
cad3d400
DM
116sub file_older_than {
117 my ($filename, $lasttime) = @_;
118
119 my $st = stat($filename);
120
121 return 0 if !defined($st);
122
123 return ($lasttime >= $st->ctime);
124}
125
758c7b6b
DM
126sub extract_filename {
127 my ($head) = @_;
128
129 if (my $value = $head->recommended_filename()) {
8210c7fa 130 chomp $value;
758c7b6b
DM
131 if (my $decvalue = MIME::Words::decode_mimewords($value)) {
132 $decvalue =~ s/\0/ /g;
5953119e 133 $decvalue = PVE::Tools::trim($decvalue);
758c7b6b
DM
134 return $decvalue;
135 }
136 }
137
138 return undef;
139}
140
141sub remove_marks {
142 my ($entity, $add_id, $id) = @_;
143
144 $id //= 1;
145
146 foreach my $tag (grep {/^x-proxmox-tmp/i} $entity->head->tags) {
147 $entity->head->delete ($tag);
148 }
149
150 $entity->head->replace('X-Proxmox-tmp-AID', $id) if $add_id;
151
152 foreach my $part ($entity->parts) {
153 $id = remove_marks($part, $add_id, $id + 1);
154 }
155
156 return $id;
157}
158
159sub subst_values {
160 my ($body, $dh) = @_;
161
162 return if !$body;
163
164 foreach my $k (keys %$dh) {
165 my $v = $dh->{$k};
bd98f5a1
DM
166 if (defined($v)) {
167 $body =~ s/__\Q${k}\E__/$v/gs;
758c7b6b
DM
168 }
169 }
170
171 return $body;
172}
173
174sub reinject_mail {
175 my ($entity, $sender, $targets, $xforward, $me, $nodsn) = @_;
176
177 my $smtp;
178 my $resid;
179 my $rescode;
180 my $resmess;
181
182 eval {
183 my $smtp = Net::SMTP->new('127.0.0.1', Port => 10025, Hello => $me) ||
184 die "unable to connect to localhost at port 10025";
185
186 if (defined($xforward)) {
187 my $xfwd;
8210c7fa 188
758c7b6b
DM
189 foreach my $attr (keys %{$xforward}) {
190 $xfwd .= " $attr=$xforward->{$attr}";
191 }
192
193 if ($xfwd && $smtp->command("XFORWARD", $xfwd)->response() != CMD_OK) {
194 syslog('err', "xforward error - got: %s %s", $smtp->code, scalar($smtp->message));
195 }
196 }
197
198 if (!$smtp->mail($sender)) {
199 syslog('err', "smtp error - got: %s %s", $smtp->code, scalar ($smtp->message));
200 die "smtp from: ERROR";
201 }
202
203 my $dsnopts = $nodsn ? {Notify => ['NEVER']} : {};
204
205 if (!$smtp->to (@$targets, $dsnopts)) {
206 syslog ('err', "smtp error - got: %s %s", $smtp->code, scalar($smtp->message));
207 die "smtp to: ERROR";
208 }
209
210 # Output the head:
211 #$entity->sync_headers ();
212 $smtp->data();
213
214 my $out = PMG::SMTPPrinter->new($smtp);
215 $entity->print($out);
8210c7fa
DM
216
217 # make sure we always have a newline at the end of the mail
758c7b6b
DM
218 # else dataend() fails
219 $smtp->datasend("\n");
220
221 if ($smtp->dataend()) {
222 my @msgs = $smtp->message;
8210c7fa
DM
223 $resmess = $msgs[$#msgs];
224 ($resid) = $resmess =~ m/Ok: queued as ([0-9A-Z]+)/;
758c7b6b
DM
225 $rescode = $smtp->code;
226 if (!$resid) {
227 die sprintf("unexpected SMTP result - got: %s %s : WARNING", $smtp->code, $resmess);
8210c7fa 228 }
758c7b6b
DM
229 } else {
230 my @msgs = $smtp->message;
8210c7fa 231 $resmess = $msgs[$#msgs];
758c7b6b
DM
232 $rescode = $smtp->code;
233 die sprintf("sending data failed - got: %s %s : ERROR", $smtp->code, $resmess);
234 }
235 };
236 my $err = $@;
8210c7fa 237
758c7b6b 238 $smtp->quit if $smtp;
8210c7fa 239
758c7b6b
DM
240 if ($err) {
241 syslog ('err', $err);
242 }
243
244 return wantarray ? ($resid, $rescode, $resmess) : $resid;
245}
246
8210c7fa
DM
247sub analyze_virus_clam {
248 my ($queue, $dname, $pmg_cfg) = @_;
249
250 my $timeout = 60*5;
251 my $vinfo;
252
253 my $clamdscan_opts = "--stdout";
254
255 my ($csec, $usec) = gettimeofday();
256
257 my $previous_alarm;
258
259 eval {
260
261 $previous_alarm = alarm($timeout);
262
263 $SIG{ALRM} = sub {
264 die "$queue->{logid}: Maximum time ($timeout sec) exceeded. " .
265 "virus analyze (clamav) failed: ERROR";
266 };
267
268 open(CMD, "/usr/bin/clamdscan $clamdscan_opts '$dname'|") ||
269 die "$queue->{logid}: can't exec clamdscan: $! : ERROR";
270
271 my $ifiles;
8210c7fa 272
54eaf7df
DM
273 my $response = '';
274 while (defined(my $line = <CMD>)) {
275 if ($line =~ m/^$dname.*:\s+([^ :]*)\s+FOUND$/) {
8210c7fa
DM
276 # we just use the first detected virus name
277 $vinfo = $1 if !$vinfo;
54eaf7df 278 } elsif ($line =~ m/^Infected files:\s(\d*)$/i) {
8210c7fa
DM
279 $ifiles = $1;
280 }
281
54eaf7df 282 $response .= $line;
8210c7fa
DM
283 }
284
285 close(CMD);
286
287 alarm(0); # avoid race conditions
288
289 if (!defined($ifiles)) {
290 die "$queue->{logid}: got undefined output from " .
54eaf7df 291 "virus detector: $response : ERROR";
8210c7fa
DM
292 }
293
294 if ($vinfo) {
54eaf7df 295 syslog('info', "$queue->{logid}: virus detected: $vinfo (clamav)");
8210c7fa
DM
296 }
297 };
298 my $err = $@;
299
300 alarm($previous_alarm);
301
302 my ($csec_end, $usec_end) = gettimeofday();
303 $queue->{ptime_clam} =
304 int (($csec_end-$csec)*1000 + ($usec_end - $usec)/1000);
305
306 if ($err) {
307 syslog ('err', $err);
308 $vinfo = undef;
309 $queue->{errors} = 1;
310 }
311
312 $queue->{vinfo_clam} = $vinfo;
313
314 return $vinfo ? "$vinfo (clamav)" : undef;
315}
316
317sub analyze_virus {
318 my ($queue, $filename, $pmg_cfg, $testmode) = @_;
319
320 # TODO: support other virus scanners?
321
322 # always scan with clamav
323 return analyze_virus_clam($queue, $filename, $pmg_cfg);
324}
325
26357b0a
DM
326sub magic_mime_type_for_file {
327 my ($filename) = @_;
dff363d9 328
26357b0a
DM
329 # we do not use get_mime_type_for_file, because that considers
330 # filename extensions - we only want magic type detection
331
332 my $bufsize = Xdgmime::xdg_mime_get_max_buffer_extents();
333 die "got strange value for max_buffer_extents" if $bufsize > 4096*10;
334
335 my $ct = "application/octet-stream";
336
dff363d9 337 my $fh = IO::File->new("<$filename") ||
26357b0a
DM
338 die "unable to open file '$filename' - $!";
339
340 my ($buf, $len);
341 if (($len = $fh->read($buf, $bufsize)) > 0) {
342 $ct = xdg_mime_get_mime_type_for_data($buf, $len);
343 }
344 $fh->close();
dff363d9 345
26357b0a 346 die "unable to read file '$filename' - $!" if ($len < 0);
dff363d9 347
26357b0a
DM
348 return $ct;
349}
758c7b6b 350
1d4193a1
DM
351sub add_ct_marks {
352 my ($entity) = @_;
353
354 if (my $path = $entity->{PMX_decoded_path}) {
355
356 # set a reasonable default if magic does not give a result
357 $entity->{PMX_magic_ct} = $entity->head->mime_attr('content-type');
358
359 if (my $ct = magic_mime_type_for_file($path)) {
360 if ($ct ne 'application/octet-stream' || !$entity->{PMX_magic_ct}) {
361 $entity->{PMX_magic_ct} = $ct;
362 }
363 }
364
365 my $filename = $entity->head->recommended_filename;
366 $filename = basename($path) if !defined($filename) || $filename eq '';
367
368 if (my $ct = xdg_mime_get_mime_type_from_file_name($filename)) {
369 $entity->{PMX_glob_ct} = $ct;
370 }
371 }
372
373 foreach my $part ($entity->parts) {
374 add_ct_marks ($part);
375 }
376}
377
f609bf7f
DM
378# x509 certificate utils
379
896ef634
DM
380# only write output if something fails
381sub run_silent_cmd {
382 my ($cmd) = @_;
383
384 my $outbuf = '';
385
386 my $record_output = sub {
387 $outbuf .= shift;
388 $outbuf .= "\n";
389 };
390
391 eval {
392 PVE::Tools::run_command($cmd, outfunc => $record_output,
393 errfunc => $record_output);
394 };
395 my $err = $@;
396
397 if ($err) {
398 print STDERR $outbuf;
399 die $err;
400 }
401}
402
3278b571 403my $proxmox_tls_cert_fn = "/etc/pmg/pmg-tls.pem";
f609bf7f
DM
404
405sub gen_proxmox_tls_cert {
bc44eb02 406 my ($force) = @_;
f609bf7f 407
bc44eb02
DM
408 my $resolv = PVE::INotify::read_file('resolvconf');
409 my $domain = $resolv->{search};
410
411 my $company = $domain; # what else ?
412 my $cn = "*.$domain";
413
414 return if !$force && -f $proxmox_tls_cert_fn;
f609bf7f
DM
415
416 my $sslconf = <<__EOD__;
417RANDFILE = /root/.rnd
418extensions = v3_req
419
420[ req ]
421default_bits = 4096
422distinguished_name = req_distinguished_name
423req_extensions = v3_req
424prompt = no
425string_mask = nombstr
426
427[ req_distinguished_name ]
428organizationalUnitName = Proxmox Mail Gateway
429organizationName = $company
430commonName = $cn
431
432[ v3_req ]
433basicConstraints = CA:FALSE
434nsCertType = server
435keyUsage = nonRepudiation, digitalSignature, keyEncipherment
436__EOD__
437
3278b571 438 my $cfgfn = "/tmp/pmgtlsconf-$$.tmp";
f609bf7f
DM
439 my $fh = IO::File->new ($cfgfn, "w");
440 print $fh $sslconf;
441 close ($fh);
442
443 eval {
896ef634
DM
444 my $cmd = ['openssl', 'req', '-batch', '-x509', '-new', '-sha256',
445 '-config', $cfgfn, '-days', 3650, '-nodes',
446 '-out', $proxmox_tls_cert_fn,
447 '-keyout', $proxmox_tls_cert_fn];
448 run_silent_cmd($cmd);
f609bf7f
DM
449 };
450
451 if (my $err = $@) {
452 unlink $proxmox_tls_cert_fn;
453 unlink $cfgfn;
454 die "unable to generate proxmox certificate request:\n$err";
455 }
456
457 unlink $cfgfn;
458}
459
460sub find_local_network_for_ip {
461 my ($ip) = @_;
462
463 my $testip = Net::IP->new($ip);
464
465 my $isv6 = $testip->version == 6;
466 my $routes = $isv6 ?
467 PVE::ProcFSTools::read_proc_net_ipv6_route() :
468 PVE::ProcFSTools::read_proc_net_route();
469
470 foreach my $entry (@$routes) {
471 my $mask;
472 if ($isv6) {
473 $mask = $entry->{prefix};
474 next if !$mask; # skip the default route...
475 } else {
476 $mask = $PVE::Network::ipv4_mask_hash_localnet->{$entry->{mask}};
477 next if !defined($mask);
478 }
479 my $cidr = "$entry->{dest}/$mask";
480 my $testnet = Net::IP->new($cidr);
481 my $overlap = $testnet->overlaps($testip);
482 if ($overlap == $Net::IP::IP_B_IN_A_OVERLAP ||
483 $overlap == $Net::IP::IP_IDENTICAL)
484 {
485 return $cidr;
486 }
487 }
488
489 die "unable to detect local network for ip '$ip'\n";
490}
d0d91cda 491
3f85510e
DM
492sub service_cmd {
493 my ($service, $cmd) = @_;
494
495 die "unknown service command '$cmd'\n"
496 if $cmd !~ m/^(start|stop|restart|reload)$/;
497
498 if ($service eq 'pmgdaemon' || $service eq 'pmgproxy') {
499 if ($cmd eq 'restart') {
500 # OK
501 } else {
502 die "invalid service cmd '$service $cmd': ERROR";
503 }
504 }
505
aab6fe36 506 $service = 'postfix@-' if $service eq 'postfix';
3f85510e
DM
507 PVE::Tools::run_command(['systemctl', $cmd, $service]);
508};
509
9f67f5b3
DM
510# this is also used to get the IP of the local node
511sub lookup_node_ip {
512 my ($nodename, $noerr) = @_;
513
514 my ($family, $packed_ip);
515
516 eval {
517 my @res = PVE::Tools::getaddrinfo_all($nodename);
518 $family = $res[0]->{family};
519 $packed_ip = (PVE::Tools::unpack_sockaddr_in46($res[0]->{addr}))[2];
520 };
521
522 if ($@) {
523 die "hostname lookup failed:\n$@" if !$noerr;
524 return undef;
525 }
526
527 my $ip = Socket::inet_ntop($family, $packed_ip);
528 if ($ip =~ m/^127\.|^::1$/) {
529 die "hostname lookup failed - got local IP address ($nodename = $ip)\n" if !$noerr;
530 return undef;
531 }
532
533 return wantarray ? ($ip, $family) : $ip;
534}
535
630d1ae3
DM
536sub run_postmap {
537 my ($filename) = @_;
538
539 # make sure the file exists (else postmap fails)
540 IO::File->new($filename, 'a', 0644);
541
542 eval {
543 PVE::Tools::run_command(
544 ['/usr/sbin/postmap', $filename],
545 errmsg => "unable to update postfix table $filename");
546 };
547 my $err = $@;
548
549 warn $err if $err;
550}
551
d17c5265
DM
552sub clamav_dbstat {
553
554 my $res = [];
555
556 my $read_cvd_info = sub {
557 my ($dbname, $dbfile) = @_;
558
559 my $header;
560 my $fh = IO::File->new("<$dbfile");
561 if (!$fh) {
562 warn "cant open ClamAV Database $dbname ($dbfile) - $!\n";
563 return;
564 }
565 $fh->read($header, 512);
566 $fh->close();
567
568 ## ClamAV-VDB:16 Mar 2016 23-17 +0000:57:4218790:60:06386f34a16ebeea2733ab037f0536be:
569 if ($header =~ m/^(ClamAV-VDB):([^:]+):(\d+):(\d+):/) {
570 my ($ftype, $btime, $version, $nsigs) = ($1, $2, $3, $4);
571 push @$res, {
572 name => $dbname,
573 type => $ftype,
574 build_time => $btime,
575 version => $version,
576 nsigs => $nsigs,
577 };
578 } else {
579 warn "unable to parse ClamAV Database $dbname ($dbfile)\n";
580 }
581 };
582
583 # main database
584 my $filename = "/var/lib/clamav/main.inc/main.info";
585 $filename = "/var/lib/clamav/main.cvd" if ! -f $filename;
586
587 $read_cvd_info->('main', $filename) if -f $filename;
588
589 # daily database
590 $filename = "/var/lib/clamav/daily.inc/daily.info";
591 $filename = "/var/lib/clamav/daily.cvd" if ! -f $filename;
592 $filename = "/var/lib/clamav/daily.cld" if ! -f $filename;
593
594 $read_cvd_info->('daily', $filename) if -f $filename;
595
596 $filename = "/var/lib/clamav/bytecode.cvd";
597 $read_cvd_info->('bytecode', $filename) if -f $filename;
598
9860e592
DM
599 $filename = "/var/lib/clamav/safebrowsing.cvd";
600 $read_cvd_info->('safebrowsing', $filename) if -f $filename;
601
dabcd20e
DM
602 my $ss_dbs_fn = "/var/lib/clamav-unofficial-sigs/configs/ss-include-dbs.txt";
603 my $ss_dbs_files = {};
604 if (my $ssfh = IO::File->new("<${ss_dbs_fn}")) {
605 while (defined(my $line = <$ssfh>)) {
606 chomp $line;
607 $ss_dbs_files->{$line} = 1;
608 }
609 }
d17c5265
DM
610 my $last = 0;
611 my $nsigs = 0;
612 foreach $filename (</var/lib/clamav/*>) {
dabcd20e
DM
613 my $fn = basename($filename);
614 next if !$ss_dbs_files->{$fn};
615
d17c5265
DM
616 my $fh = IO::File->new("<$filename");
617 next if !defined($fh);
618 my $st = stat($fh);
619 next if !$st;
620 my $mtime = $st->mtime();
621 $last = $mtime if $mtime > $last;
622 while (defined(my $line = <$fh>)) { $nsigs++; }
623 }
624
625 if ($nsigs > 0) {
626 push @$res, {
9860e592 627 name => 'sanesecurity',
d17c5265 628 type => 'unofficial',
2d011fef 629 build_time => strftime("%d %b %Y %H-%M %z", localtime($last)),
d17c5265
DM
630 nsigs => $nsigs,
631 };
632 }
633
634 return $res;
635}
636
dff363d9
DM
637# RRD related code
638my $rrd_dir = "/var/lib/rrdcached/db";
639my $rrdcached_socket = "/var/run/rrdcached.sock";
640
641my $rrd_def_node = [
642 "DS:loadavg:GAUGE:120:0:U",
643 "DS:maxcpu:GAUGE:120:0:U",
644 "DS:cpu:GAUGE:120:0:U",
645 "DS:iowait:GAUGE:120:0:U",
646 "DS:memtotal:GAUGE:120:0:U",
647 "DS:memused:GAUGE:120:0:U",
648 "DS:swaptotal:GAUGE:120:0:U",
649 "DS:swapused:GAUGE:120:0:U",
650 "DS:roottotal:GAUGE:120:0:U",
651 "DS:rootused:GAUGE:120:0:U",
652 "DS:netin:DERIVE:120:0:U",
653 "DS:netout:DERIVE:120:0:U",
654
655 "RRA:AVERAGE:0.5:1:70", # 1 min avg - one hour
656 "RRA:AVERAGE:0.5:30:70", # 30 min avg - one day
657 "RRA:AVERAGE:0.5:180:70", # 3 hour avg - one week
658 "RRA:AVERAGE:0.5:720:70", # 12 hour avg - one month
659 "RRA:AVERAGE:0.5:10080:70", # 7 day avg - ony year
660
661 "RRA:MAX:0.5:1:70", # 1 min max - one hour
662 "RRA:MAX:0.5:30:70", # 30 min max - one day
663 "RRA:MAX:0.5:180:70", # 3 hour max - one week
664 "RRA:MAX:0.5:720:70", # 12 hour max - one month
665 "RRA:MAX:0.5:10080:70", # 7 day max - ony year
666];
667
668sub cond_create_rrd_file {
669 my ($filename, $rrddef) = @_;
670
671 return if -f $filename;
672
673 my @args = ($filename);
674
675 push @args, "--daemon" => "unix:${rrdcached_socket}"
676 if -S $rrdcached_socket;
677
678 push @args, '--step', 60;
679
680 push @args, @$rrddef;
681
682 # print "TEST: " . join(' ', @args) . "\n";
683
684 RRDs::create(@args);
685 my $err = RRDs::error;
686 die "RRD error: $err\n" if $err;
687}
688
689sub update_node_status_rrd {
690
691 my $filename = "$rrd_dir/pmg-node-v1.rrd";
692 cond_create_rrd_file($filename, $rrd_def_node);
693
694 my ($avg1, $avg5, $avg15) = PVE::ProcFSTools::read_loadavg();
695
696 my $stat = PVE::ProcFSTools::read_proc_stat();
697
698 my $netdev = PVE::ProcFSTools::read_proc_net_dev();
699
700 my ($uptime) = PVE::ProcFSTools::read_proc_uptime();
701
702 my $cpuinfo = PVE::ProcFSTools::read_cpuinfo();
703
704 my $maxcpu = $cpuinfo->{cpus};
705
706 # traffic from/to physical interface cards
707 my $netin = 0;
708 my $netout = 0;
709 foreach my $dev (keys %$netdev) {
710 next if $dev !~ m/^eth\d+$/;
711 $netin += $netdev->{$dev}->{receive};
712 $netout += $netdev->{$dev}->{transmit};
713 }
714
715 my $meminfo = PVE::ProcFSTools::read_meminfo();
716
717 my $dinfo = df('/', 1); # output is bytes
718
719 my $ctime = time();
720
721 # everything not free is considered to be used
722 my $dused = $dinfo->{blocks} - $dinfo->{bfree};
723
724 my $data = "$ctime:$avg1:$maxcpu:$stat->{cpu}:$stat->{wait}:" .
725 "$meminfo->{memtotal}:$meminfo->{memused}:" .
726 "$meminfo->{swaptotal}:$meminfo->{swapused}:" .
727 "$dinfo->{blocks}:$dused:$netin:$netout";
728
729
730 my @args = ($filename);
731
732 push @args, "--daemon" => "unix:${rrdcached_socket}"
733 if -S $rrdcached_socket;
734
735 push @args, $data;
736
737 # print "TEST: " . join(' ', @args) . "\n";
738
739 RRDs::update(@args);
740 my $err = RRDs::error;
741 die "RRD error: $err\n" if $err;
742}
743
065b2986
DM
744sub create_rrd_data {
745 my ($rrdname, $timeframe, $cf) = @_;
746
747 my $rrd = "${rrd_dir}/$rrdname";
748
749 my $setup = {
750 hour => [ 60, 70 ],
751 day => [ 60*30, 70 ],
752 week => [ 60*180, 70 ],
753 month => [ 60*720, 70 ],
754 year => [ 60*10080, 70 ],
755 };
756
757 my ($reso, $count) = @{$setup->{$timeframe}};
758 my $ctime = $reso*int(time()/$reso);
759 my $req_start = $ctime - $reso*$count;
760
761 $cf = "AVERAGE" if !$cf;
762
763 my @args = (
764 "-s" => $req_start,
765 "-e" => $ctime - 1,
766 "-r" => $reso,
767 );
768
769 push @args, "--daemon" => "unix:${rrdcached_socket}"
770 if -S $rrdcached_socket;
771
772 my ($start, $step, $names, $data) = RRDs::fetch($rrd, $cf, @args);
773
774 my $err = RRDs::error;
775 die "RRD error: $err\n" if $err;
776
777 die "got wrong time resolution ($step != $reso)\n"
778 if $step != $reso;
779
780 my $res = [];
781 my $fields = scalar(@$names);
782 for my $line (@$data) {
783 my $entry = { 'time' => $start };
784 $start += $step;
785 for (my $i = 0; $i < $fields; $i++) {
786 my $name = $names->[$i];
787 if (defined(my $val = $line->[$i])) {
788 $entry->{$name} = $val;
789 } else {
790 # leave empty fields undefined
791 # maybe make this configurable?
792 }
793 }
794 push @$res, $entry;
795 }
796
797 return $res;
798}
799
758c7b6b 8001;