bump version to 5.4-15
[pve-manager.git] / PVE / API2 / APT.pm
1 package PVE::API2::APT;
2
3 use strict;
4 use warnings;
5
6 use POSIX;
7 use File::stat ();
8 use IO::File;
9 use File::Basename;
10
11 use LWP::UserAgent;
12
13 use PVE::pvecfg;
14 use PVE::Tools qw(extract_param);
15 use PVE::Cluster;
16 use PVE::SafeSyslog;
17 use PVE::INotify;
18 use PVE::Exception;
19 use PVE::RESTHandler;
20 use PVE::RPCEnvironment;
21 use PVE::API2Tools;
22
23 use JSON;
24 use PVE::JSONSchema qw(get_standard_option);
25
26 use AptPkg::Cache;
27 use AptPkg::PkgRecords;
28 use AptPkg::System;
29
30 my $get_apt_cache = sub {
31
32 my $apt_cache = AptPkg::Cache->new() || die "unable to initialize AptPkg::Cache\n";
33
34 return $apt_cache;
35 };
36
37 use base qw(PVE::RESTHandler);
38
39 __PACKAGE__->register_method({
40 name => 'index',
41 path => '',
42 method => 'GET',
43 description => "Directory index for apt (Advanced Package Tool).",
44 permissions => {
45 user => 'all',
46 },
47 parameters => {
48 additionalProperties => 0,
49 properties => {
50 node => get_standard_option('pve-node'),
51 },
52 },
53 returns => {
54 type => "array",
55 items => {
56 type => "object",
57 properties => {
58 id => { type => 'string' },
59 },
60 },
61 links => [ { rel => 'child', href => "{id}" } ],
62 },
63 code => sub {
64 my ($param) = @_;
65
66 my $res = [
67 { id => 'changelog' },
68 { id => 'update' },
69 { id => 'versions' },
70 ];
71
72 return $res;
73 }});
74
75 my $get_pkgfile = sub {
76 my ($veriter) = @_;
77
78 foreach my $verfile (@{$veriter->{FileList}}) {
79 my $pkgfile = $verfile->{File};
80 next if !$pkgfile->{Origin};
81 return $pkgfile;
82 }
83
84 return undef;
85 };
86
87 my $get_changelog_url =sub {
88 my ($pkgname, $info, $pkgver, $origin, $component) = @_;
89
90 my $changelog_url;
91 my $base = dirname($info->{FileName});
92 if ($origin && $base) {
93 $pkgver =~ s/^\d+://; # strip epoch
94 my $srcpkg = $info->{SourcePkg} || $pkgname;
95 if ($origin eq 'Debian') {
96 $base =~ s!pool/updates/!pool/!; # for security channel
97 $changelog_url = "http://packages.debian.org/changelogs/$base/" .
98 "${srcpkg}_${pkgver}/changelog";
99 } elsif ($origin eq 'Proxmox') {
100 if ($component eq 'pve-enterprise') {
101 $changelog_url = "https://enterprise.proxmox.com/debian/$base/" .
102 "${pkgname}_${pkgver}.changelog";
103 } else {
104 $changelog_url = "http://download.proxmox.com/debian/$base/" .
105 "${pkgname}_${pkgver}.changelog";
106 }
107 }
108 }
109
110 return $changelog_url;
111 };
112
113 my $assemble_pkginfo = sub {
114 my ($pkgname, $info, $current_ver, $candidate_ver) = @_;
115
116 my $data = {
117 Package => $info->{Name},
118 Title => $info->{ShortDesc},
119 Origin => 'unknown',
120 };
121
122 if (my $pkgfile = &$get_pkgfile($candidate_ver)) {
123 $data->{Origin} = $pkgfile->{Origin};
124 if (my $changelog_url = &$get_changelog_url($pkgname, $info, $candidate_ver->{VerStr},
125 $pkgfile->{Origin}, $pkgfile->{Component})) {
126 $data->{ChangeLogUrl} = $changelog_url;
127 }
128 }
129
130 if (my $desc = $info->{LongDesc}) {
131 $desc =~ s/^.*\n\s?//; # remove first line
132 $desc =~ s/\n / /g;
133 $data->{Description} = $desc;
134 }
135
136 foreach my $k (qw(Section Arch Priority)) {
137 $data->{$k} = $candidate_ver->{$k};
138 }
139
140 $data->{Version} = $candidate_ver->{VerStr};
141 $data->{OldVersion} = $current_ver->{VerStr} if $current_ver;
142
143 return $data;
144 };
145
146 # we try to cache results
147 my $pve_pkgstatus_fn = "/var/lib/pve-manager/pkgupdates";
148
149 my $read_cached_pkgstatus = sub {
150 my $data = [];
151 eval {
152 my $jsonstr = PVE::Tools::file_get_contents($pve_pkgstatus_fn, 5*1024*1024);
153 $data = decode_json($jsonstr);
154 };
155 if (my $err = $@) {
156 warn "error reading cached package status in $pve_pkgstatus_fn\n";
157 }
158 return $data;
159 };
160
161 my $update_pve_pkgstatus = sub {
162
163 syslog('info', "update new package list: $pve_pkgstatus_fn");
164
165 my $notify_status = {};
166 my $oldpkglist = &$read_cached_pkgstatus();
167 foreach my $pi (@$oldpkglist) {
168 $notify_status->{$pi->{Package}} = $pi->{NotifyStatus};
169 }
170
171 my $pkglist = [];
172
173 my $cache = &$get_apt_cache();
174 my $policy = $cache->policy;
175 my $pkgrecords = $cache->packages();
176
177 foreach my $pkgname (keys %$cache) {
178 my $p = $cache->{$pkgname};
179 next if !$p->{SelectedState} || ($p->{SelectedState} ne 'Install');
180 my $current_ver = $p->{CurrentVer} || next;
181 my $candidate_ver = $policy->candidate($p) || next;
182
183 if ($current_ver->{VerStr} ne $candidate_ver->{VerStr}) {
184 my $info = $pkgrecords->lookup($pkgname);
185 my $res = &$assemble_pkginfo($pkgname, $info, $current_ver, $candidate_ver);
186 push @$pkglist, $res;
187
188 # also check if we need any new package
189 # Note: this is just a quick hack (not recursive as it should be), because
190 # I found no way to get that info from AptPkg
191 if (my $deps = $candidate_ver->{DependsList}) {
192 my $found;
193 my $req;
194 for my $d (@$deps) {
195 if ($d->{DepType} eq 'Depends') {
196 $found = $d->{TargetPkg}->{SelectedState} eq 'Install' if !$found;
197 $req = $d->{TargetPkg} if !$req;
198
199 if (!($d->{CompType} & AptPkg::Dep::Or)) {
200 if (!$found && $req) { # New required Package
201 my $tpname = $req->{Name};
202 my $tpinfo = $pkgrecords->lookup($tpname);
203 my $tpcv = $policy->candidate($req);
204 if ($tpinfo && $tpcv) {
205 my $res = &$assemble_pkginfo($tpname, $tpinfo, undef, $tpcv);
206 push @$pkglist, $res;
207 }
208 }
209 undef $found;
210 undef $req;
211 }
212 }
213 }
214 }
215 }
216 }
217
218 # keep notification status (avoid sending mails abou new packages more than once)
219 foreach my $pi (@$pkglist) {
220 if (my $ns = $notify_status->{$pi->{Package}}) {
221 $pi->{NotifyStatus} = $ns if $ns eq $pi->{Version};
222 }
223 }
224
225 PVE::Tools::file_set_contents($pve_pkgstatus_fn, encode_json($pkglist));
226
227 return $pkglist;
228 };
229
230 __PACKAGE__->register_method({
231 name => 'list_updates',
232 path => 'update',
233 method => 'GET',
234 description => "List available updates.",
235 permissions => {
236 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
237 },
238 protected => 1,
239 proxyto => 'node',
240 parameters => {
241 additionalProperties => 0,
242 properties => {
243 node => get_standard_option('pve-node'),
244 },
245 },
246 returns => {
247 type => "array",
248 items => {
249 type => "object",
250 properties => {},
251 },
252 },
253 code => sub {
254 my ($param) = @_;
255
256 if (my $st1 = File::stat::stat($pve_pkgstatus_fn)) {
257 my $st2 = File::stat::stat("/var/cache/apt/pkgcache.bin");
258 my $st3 = File::stat::stat("/var/lib/dpkg/status");
259
260 if ($st2 && $st3 && $st2->mtime <= $st1->mtime && $st3->mtime <= $st1->mtime) {
261 if (my $data = &$read_cached_pkgstatus()) {
262 return $data;
263 }
264 }
265 }
266
267 my $pkglist = &$update_pve_pkgstatus();
268
269 return $pkglist;
270 }});
271
272 __PACKAGE__->register_method({
273 name => 'update_database',
274 path => 'update',
275 method => 'POST',
276 description => "This is used to resynchronize the package index files from their sources (apt-get update).",
277 permissions => {
278 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
279 },
280 protected => 1,
281 proxyto => 'node',
282 parameters => {
283 additionalProperties => 0,
284 properties => {
285 node => get_standard_option('pve-node'),
286 notify => {
287 type => 'boolean',
288 description => "Send notification mail about new packages (to email address specified for user 'root\@pam').",
289 optional => 1,
290 default => 0,
291 },
292 quiet => {
293 type => 'boolean',
294 description => "Only produces output suitable for logging, omitting progress indicators.",
295 optional => 1,
296 default => 0,
297 },
298 },
299 },
300 returns => {
301 type => 'string',
302 },
303 code => sub {
304 my ($param) = @_;
305
306 my $rpcenv = PVE::RPCEnvironment::get();
307
308 my $authuser = $rpcenv->get_user();
309
310 my $realcmd = sub {
311 my $upid = shift;
312
313 # setup proxy for apt
314 my $dcconf = PVE::Cluster::cfs_read_file('datacenter.cfg');
315
316 my $aptconf = "// no proxy configured\n";
317 if ($dcconf->{http_proxy}) {
318 $aptconf = "Acquire::http::Proxy \"$dcconf->{http_proxy}\";\n";
319 }
320 my $aptcfn = "/etc/apt/apt.conf.d/76pveproxy";
321 PVE::Tools::file_set_contents($aptcfn, $aptconf);
322
323 my $cmd = ['apt-get', 'update'];
324
325 print "starting apt-get update\n" if !$param->{quiet};
326
327 if ($param->{quiet}) {
328 PVE::Tools::run_command($cmd, outfunc => sub {}, errfunc => sub {});
329 } else {
330 PVE::Tools::run_command($cmd);
331 }
332
333 my $pkglist = &$update_pve_pkgstatus();
334
335 if ($param->{notify} && scalar(@$pkglist)) {
336
337 my $usercfg = PVE::Cluster::cfs_read_file("user.cfg");
338 my $rootcfg = $usercfg->{users}->{'root@pam'} || {};
339 my $mailto = $rootcfg->{email};
340
341 if ($mailto) {
342 my $hostname = `hostname -f` || PVE::INotify::nodename();
343 chomp $hostname;
344 my $mailfrom = $dcconf->{email_from} || "root";
345
346 my $data = "Content-Type: text/plain;charset=\"UTF8\"\n";
347 $data .= "Content-Transfer-Encoding: 8bit\n";
348 $data .= "FROM: <$mailfrom>\n";
349 $data .= "TO: $mailto\n";
350 $data .= "SUBJECT: New software packages available ($hostname)\n";
351 $data .= "\n";
352
353 $data .= "The following updates are available:\n\n";
354
355 my $count = 0;
356 foreach my $p (sort {$a->{Package} cmp $b->{Package} } @$pkglist) {
357 next if $p->{NotifyStatus} && $p->{NotifyStatus} eq $p->{Version};
358 $count++;
359 if ($p->{OldVersion}) {
360 $data .= "$p->{Package}: $p->{OldVersion} ==> $p->{Version}\n";
361 } else {
362 $data .= "$p->{Package}: $p->{Version} (new)\n";
363 }
364 }
365
366 return if !$count;
367
368 my $fh = IO::File->new("|sendmail -B 8BITMIME -f $mailfrom $mailto") ||
369 die "unable to open 'sendmail' - $!";
370
371 print $fh $data;
372
373 $fh->close() || die "unable to close 'sendmail' - $!";
374
375 foreach my $pi (@$pkglist) {
376 $pi->{NotifyStatus} = $pi->{Version};
377 }
378 PVE::Tools::file_set_contents($pve_pkgstatus_fn, encode_json($pkglist));
379 }
380 }
381
382 return;
383 };
384
385 return $rpcenv->fork_worker('aptupdate', undef, $authuser, $realcmd);
386
387 }});
388
389 __PACKAGE__->register_method({
390 name => 'changelog',
391 path => 'changelog',
392 method => 'GET',
393 description => "Get package changelogs.",
394 permissions => {
395 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
396 },
397 proxyto => 'node',
398 parameters => {
399 additionalProperties => 0,
400 properties => {
401 node => get_standard_option('pve-node'),
402 name => {
403 description => "Package name.",
404 type => 'string',
405 },
406 version => {
407 description => "Package version.",
408 type => 'string',
409 optional => 1,
410 },
411 },
412 },
413 returns => {
414 type => "string",
415 },
416 code => sub {
417 my ($param) = @_;
418
419 my $pkgname = $param->{name};
420
421 my $cache = &$get_apt_cache();
422 my $policy = $cache->policy;
423 my $p = $cache->{$pkgname} || die "no such package '$pkgname'\n";
424 my $pkgrecords = $cache->packages();
425
426 my $ver;
427 if ($param->{version}) {
428 if (my $available = $p->{VersionList}) {
429 for my $v (@$available) {
430 if ($v->{VerStr} eq $param->{version}) {
431 $ver = $v;
432 last;
433 }
434 }
435 }
436 die "package '$pkgname' version '$param->{version}' is not avalable\n" if !$ver;
437 } else {
438 $ver = $policy->candidate($p) || die "no installation candidate for package '$pkgname'\n";
439 }
440
441 my $info = $pkgrecords->lookup($pkgname);
442
443 my $pkgfile = &$get_pkgfile($ver);
444 my $url;
445
446 die "changelog for '${pkgname}_$ver->{VerStr}' not available\n"
447 if !($pkgfile && ($url = &$get_changelog_url($pkgname, $info, $ver->{VerStr}, $pkgfile->{Origin}, $pkgfile->{Component})));
448
449 my $data = "";
450
451 my $dccfg = PVE::Cluster::cfs_read_file('datacenter.cfg');
452 my $proxy = $dccfg->{http_proxy};
453
454 my $ua = LWP::UserAgent->new;
455 $ua->agent("PVE/1.0");
456 $ua->timeout(10);
457 $ua->max_size(1024*1024);
458 $ua->ssl_opts(verify_hostname => 0); # don't care for changelogs
459
460 if ($proxy) {
461 $ua->proxy(['http', 'https'], $proxy);
462 } else {
463 $ua->env_proxy;
464 }
465
466 my $username;
467 my $pw;
468
469 if ($pkgfile->{Origin} eq 'Proxmox' && $pkgfile->{Component} eq 'pve-enterprise') {
470 my $info = PVE::INotify::read_file('subscription');
471 if ($info->{status} eq 'Active') {
472 $username = $info->{key};
473 $pw = PVE::API2Tools::get_hwaddress();
474 $ua->credentials("enterprise.proxmox.com:443", 'pve-enterprise-repository',
475 $username, $pw);
476 }
477 }
478
479 my $response = $ua->get($url);
480
481 if ($response->is_success) {
482 $data = $response->decoded_content;
483 } else {
484 PVE::Exception::raise($response->message, code => $response->code);
485 }
486
487 return $data;
488 }});
489
490 __PACKAGE__->register_method({
491 name => 'versions',
492 path => 'versions',
493 method => 'GET',
494 proxyto => 'node',
495 description => "Get package information for important Proxmox packages.",
496 permissions => {
497 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
498 },
499 parameters => {
500 additionalProperties => 0,
501 properties => {
502 node => get_standard_option('pve-node'),
503 },
504 },
505 returns => {
506 type => "array",
507 items => {
508 type => "object",
509 properties => {},
510 },
511 },
512 code => sub {
513 my ($param) = @_;
514
515 my $cache = &$get_apt_cache();
516 my $policy = $cache->policy;
517 my $pkgrecords = $cache->packages();
518
519 # order most important things first
520 my @list = qw(proxmox-ve pve-manager);
521
522 my $aptver = $AptPkg::System::_system->versioning();
523 my $byver = sub { $aptver->compare($cache->{$b}->{CurrentVer}->{VerStr}, $cache->{$a}->{CurrentVer}->{VerStr}) };
524 push @list, sort $byver grep { /^pve-kernel-/ && $cache->{$_}->{CurrentState} eq 'Installed' } keys %$cache;
525
526 my @opt_pack = qw(
527 ceph
528 gfs2-utils
529 libpve-apiclient-perl
530 openvswitch-switch
531 pve-sheepdog
532 pve-zsync
533 zfsutils-linux
534 );
535
536 my @pkgs = qw(
537 corosync
538 criu
539 libjs-extjs
540 glusterfs-client
541 ksm-control-daemon
542 libpve-access-control
543 libpve-common-perl
544 libpve-guest-common-perl
545 libpve-http-server-perl
546 libpve-storage-perl
547 libqb0
548 lvm2
549 lxc-pve
550 lxcfs
551 novnc-pve
552 proxmox-widget-toolkit
553 pve-cluster
554 pve-container
555 pve-docs
556 pve-edk2-firmware
557 pve-firewall
558 pve-firmware
559 pve-ha-manager
560 pve-i18n
561 pve-libspice-server1
562 pve-qemu-kvm
563 pve-xtermjs
564 qemu-server
565 smartmontools
566 spiceterm
567 vncterm
568 );
569
570 # add the rest ordered by name, easier to find for humans
571 push @list, (sort @pkgs, @opt_pack);
572
573 my (undef, undef, $kernel_release) = POSIX::uname();
574 my $pvever = PVE::pvecfg::version_text();
575
576 my $pkglist = [];
577 foreach my $pkgname (@list) {
578 my $p = $cache->{$pkgname};
579 my $info = $pkgrecords->lookup($pkgname);
580 my $candidate_ver = defined($p) ? $policy->candidate($p) : undef;
581 my $res;
582 if (my $current_ver = $p->{CurrentVer}) {
583 $res = &$assemble_pkginfo($pkgname, $info, $current_ver,
584 $candidate_ver || $current_ver);
585 } elsif ($candidate_ver) {
586 $res = &$assemble_pkginfo($pkgname, $info, $candidate_ver,
587 $candidate_ver);
588 delete $res->{OldVersion};
589 } else {
590 next;
591 }
592 $res->{CurrentState} = $p->{CurrentState};
593
594 # hack: add some useful information (used by 'pveversion -v')
595 if ($pkgname eq 'pve-manager') {
596 $res->{ManagerVersion} = $pvever;
597 } elsif ($pkgname eq 'proxmox-ve') {
598 $res->{RunningKernel} = $kernel_release;
599 }
600 if (grep( /^$pkgname$/, @opt_pack)) {
601 next if $res->{CurrentState} eq 'NotInstalled';
602 }
603
604 push @$pkglist, $res;
605 }
606
607 return $pkglist;
608 }});
609
610 1;