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