]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/APT.pm
bcd749b245febe6523af4e47ad9522507b223269
[pmg-api.git] / src / 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 Proxmox::RS::APT::Repositories;
25
26 use AptPkg::Cache;
27 use AptPkg::Version;
28 use AptPkg::PkgRecords;
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 => 'repositories' },
69 { id => 'update' },
70 { id => 'versions' },
71 ];
72
73 return $res;
74 }});
75
76 my $get_pkgfile = sub {
77 my ($veriter) = @_;
78
79 foreach my $verfile (@{$veriter->{FileList}}) {
80 my $pkgfile = $verfile->{File};
81 next if !$pkgfile->{Origin};
82 return $pkgfile;
83 }
84
85 return undef;
86 };
87
88 my $get_changelog_url =sub {
89 my ($pkgname, $info, $pkgver, $origin, $component) = @_;
90
91 my $changelog_url;
92 my $base = dirname($info->{FileName});
93 if ($origin && $base) {
94 $pkgver =~ s/^\d+://; # strip epoch
95 my $srcpkg = $info->{SourcePkg} || $pkgname;
96 if ($origin eq 'Debian') {
97 $base =~ s!pool/updates/!pool/!; # for security channel
98 $changelog_url = "http://packages.debian.org/changelogs/$base/" .
99 "${srcpkg}_${pkgver}/changelog";
100 } elsif ($origin eq 'Proxmox') {
101 if ($component eq 'pmg-enterprise') {
102 $changelog_url = "https://enterprise.proxmox.com/debian/pmg/$base/" .
103 "${pkgname}_${pkgver}.changelog";
104 } else {
105 $changelog_url = "http://download.proxmox.com/debian/pmg/$base/" .
106 "${pkgname}_${pkgver}.changelog";
107 }
108 }
109 }
110
111 return $changelog_url;
112 };
113
114 my $assemble_pkginfo = sub {
115 my ($pkgname, $info, $current_ver, $candidate_ver) = @_;
116
117 my $data = {
118 Package => $info->{Name},
119 Title => $info->{ShortDesc},
120 Origin => 'unknown',
121 };
122
123 if (my $pkgfile = &$get_pkgfile($candidate_ver)) {
124 $data->{Origin} = $pkgfile->{Origin};
125 if (my $changelog_url = &$get_changelog_url($pkgname, $info, $candidate_ver->{VerStr},
126 $pkgfile->{Origin}, $pkgfile->{Component})) {
127 $data->{ChangeLogUrl} = $changelog_url;
128 }
129 }
130
131 if (my $desc = $info->{LongDesc}) {
132 $desc =~ s/^.*\n\s?//; # remove first line
133 $desc =~ s/\n / /g;
134 $data->{Description} = $desc;
135 }
136
137 foreach my $k (qw(Section Arch Priority)) {
138 $data->{$k} = $candidate_ver->{$k};
139 }
140
141 $data->{Version} = $candidate_ver->{VerStr};
142 $data->{OldVersion} = $current_ver->{VerStr} if $current_ver;
143
144 return $data;
145 };
146
147 # we try to cache results
148 my $pmg_pkgstatus_fn = "/var/lib/pmg/pkgupdates";
149
150 my $read_cached_pkgstatus = sub {
151 my $data = [];
152 eval {
153 my $jsonstr = PVE::Tools::file_get_contents($pmg_pkgstatus_fn, 5*1024*1024);
154 $data = decode_json($jsonstr);
155 };
156 if (my $err = $@) {
157 warn "error reading cached package status in $pmg_pkgstatus_fn\n";
158 }
159 return $data;
160 };
161
162 my $update_pmg_pkgstatus = sub {
163
164 syslog('info', "update new package list: $pmg_pkgstatus_fn");
165
166 my $notify_status = {};
167 my $oldpkglist = &$read_cached_pkgstatus();
168 foreach my $pi (@$oldpkglist) {
169 $notify_status->{$pi->{Package}} = $pi->{NotifyStatus};
170 }
171
172 my $pkglist = [];
173
174 my $cache = &$get_apt_cache();
175 my $policy = $cache->policy;
176 my $pkgrecords = $cache->packages();
177
178 foreach my $pkgname (keys %$cache) {
179 my $p = $cache->{$pkgname};
180 next if !$p->{SelectedState} || ($p->{SelectedState} ne 'Install');
181 my $current_ver = $p->{CurrentVer} || next;
182 my $candidate_ver = $policy->candidate($p) || next;
183
184 if ($current_ver->{VerStr} ne $candidate_ver->{VerStr}) {
185 my $info = $pkgrecords->lookup($pkgname);
186 my $res = &$assemble_pkginfo($pkgname, $info, $current_ver, $candidate_ver);
187 push @$pkglist, $res;
188
189 # also check if we need any new package
190 # Note: this is just a quick hack (not recursive as it should be), because
191 # I found no way to get that info from AptPkg
192 if (my $deps = $candidate_ver->{DependsList}) {
193 my $found;
194 my $req;
195 for my $d (@$deps) {
196 if ($d->{DepType} eq 'Depends') {
197 $found = $d->{TargetPkg}->{SelectedState} eq 'Install' if !$found;
198 $req = $d->{TargetPkg} if !$req;
199
200 if (!($d->{CompType} & AptPkg::Dep::Or)) {
201 if (!$found && $req) { # New required Package
202 my $tpname = $req->{Name};
203 my $tpinfo = $pkgrecords->lookup($tpname);
204 my $tpcv = $policy->candidate($req);
205 if ($tpinfo && $tpcv) {
206 my $res = &$assemble_pkginfo($tpname, $tpinfo, undef, $tpcv);
207 push @$pkglist, $res;
208 }
209 }
210 undef $found;
211 undef $req;
212 }
213 }
214 }
215 }
216 }
217 }
218
219 # keep notification status (avoid sending mails abou new packages more than once)
220 foreach my $pi (@$pkglist) {
221 if (my $ns = $notify_status->{$pi->{Package}}) {
222 $pi->{NotifyStatus} = $ns if $ns eq $pi->{Version};
223 }
224 }
225
226 PVE::Tools::file_set_contents($pmg_pkgstatus_fn, encode_json($pkglist));
227
228 return $pkglist;
229 };
230
231 __PACKAGE__->register_method({
232 name => 'list_updates',
233 path => 'update',
234 method => 'GET',
235 description => "List available updates.",
236 protected => 1,
237 proxyto => 'node',
238 permissions => { check => [ 'admin', 'audit' ] },
239 parameters => {
240 additionalProperties => 0,
241 properties => {
242 node => get_standard_option('pve-node'),
243 },
244 },
245 returns => {
246 type => "array",
247 items => {
248 type => "object",
249 properties => {},
250 },
251 },
252 code => sub {
253 my ($param) = @_;
254
255 if (my $st1 = File::stat::stat($pmg_pkgstatus_fn)) {
256 my $st2 = File::stat::stat("/var/cache/apt/pkgcache.bin");
257 my $st3 = File::stat::stat("/var/lib/dpkg/status");
258
259 if ($st2 && $st3 && $st2->mtime <= $st1->mtime && $st3->mtime <= $st1->mtime) {
260 if (my $data = &$read_cached_pkgstatus()) {
261 return $data;
262 }
263 }
264 }
265
266 my $pkglist = &$update_pmg_pkgstatus();
267
268 return $pkglist;
269 }});
270
271 __PACKAGE__->register_method({
272 name => 'update_database',
273 path => 'update',
274 method => 'POST',
275 description => "This is used to resynchronize the package index files from their sources (apt-get update).",
276 protected => 1,
277 proxyto => 'node',
278 permissions => { check => [ 'admin' ] },
279 parameters => {
280 additionalProperties => 0,
281 properties => {
282 node => get_standard_option('pve-node'),
283 notify => {
284 type => 'boolean',
285 description => "Send notification mail about new packages (to email address specified for user 'root\@pam').",
286 optional => 1,
287 default => 0,
288 },
289 quiet => {
290 type => 'boolean',
291 description => "Only produces output suitable for logging, omitting progress indicators.",
292 optional => 1,
293 default => 0,
294 },
295 },
296 },
297 returns => {
298 type => 'string',
299 },
300 code => sub {
301 my ($param) = @_;
302
303 my $rpcenv = PMG::RESTEnvironment->get();
304
305 my $authuser = $rpcenv->get_user();
306
307 my $realcmd = sub {
308 my $upid = shift;
309
310 my $pmg_cfg = PMG::Config->new();
311
312 my $http_proxy = $pmg_cfg->get('admin', 'http_proxy');
313 my $aptconf = "// no proxy configured\n";
314 if ($http_proxy) {
315 $aptconf = "Acquire::http::Proxy \"${http_proxy}\";\n";
316 }
317 my $aptcfn = "/etc/apt/apt.conf.d/76pmgproxy";
318 PVE::Tools::file_set_contents($aptcfn, $aptconf);
319
320 my $cmd = ['apt-get', 'update'];
321
322 print "starting apt-get update\n" if !$param->{quiet};
323
324 if ($param->{quiet}) {
325 PVE::Tools::run_command($cmd, outfunc => sub {}, errfunc => sub {});
326 } else {
327 PVE::Tools::run_command($cmd);
328 }
329
330 my $pkglist = &$update_pmg_pkgstatus();
331
332 if ($param->{notify} && scalar(@$pkglist)) {
333
334 my $mailfrom = "root";
335
336 if (my $mailto = $pmg_cfg->get('admin', 'email', 1)) {
337
338 my $text .= "The following updates are available:\n\n";
339
340 my $count = 0;
341 foreach my $p (sort {$a->{Package} cmp $b->{Package} } @$pkglist) {
342 next if $p->{NotifyStatus} && $p->{NotifyStatus} eq $p->{Version};
343 $count++;
344 if ($p->{OldVersion}) {
345 $text .= "$p->{Package}: $p->{OldVersion} ==> $p->{Version}\n";
346 } else {
347 $text .= "$p->{Package}: $p->{Version} (new)\n";
348 }
349 }
350
351 return if !$count;
352
353 my $hostname = `hostname -f` || PVE::INotify::nodename();
354 chomp $hostname;
355
356 my $subject = "New software packages available ($hostname)";
357 PVE::Tools::sendmail($mailto, $subject, $text, undef,
358 $mailfrom, 'Proxmox Mail Gateway');
359
360 foreach my $pi (@$pkglist) {
361 $pi->{NotifyStatus} = $pi->{Version};
362 }
363
364 PVE::Tools::file_set_contents($pmg_pkgstatus_fn, encode_json($pkglist));
365 }
366 }
367
368 return;
369 };
370
371 return $rpcenv->fork_worker('aptupdate', undef, $authuser, $realcmd);
372
373 }});
374
375 __PACKAGE__->register_method({
376 name => 'changelog',
377 path => 'changelog',
378 method => 'GET',
379 description => "Get package changelogs.",
380 proxyto => 'node',
381 permissions => { check => [ 'admin', 'audit' ] },
382 parameters => {
383 additionalProperties => 0,
384 properties => {
385 node => get_standard_option('pve-node'),
386 name => {
387 description => "Package name.",
388 type => 'string',
389 },
390 version => {
391 description => "Package version.",
392 type => 'string',
393 optional => 1,
394 },
395 },
396 },
397 returns => {
398 type => "string",
399 },
400 code => sub {
401 my ($param) = @_;
402
403 my $pkgname = $param->{name};
404
405 my $cache = &$get_apt_cache();
406 my $policy = $cache->policy;
407 my $p = $cache->{$pkgname} || die "no such package '$pkgname'\n";
408 my $pkgrecords = $cache->packages();
409
410 my $ver;
411 if ($param->{version}) {
412 if (my $available = $p->{VersionList}) {
413 for my $v (@$available) {
414 if ($v->{VerStr} eq $param->{version}) {
415 $ver = $v;
416 last;
417 }
418 }
419 }
420 die "package '$pkgname' version '$param->{version}' is not available\n" if !$ver;
421 } else {
422 $ver = $policy->candidate($p) || die "no installation candidate for package '$pkgname'\n";
423 }
424
425 my $info = $pkgrecords->lookup($pkgname);
426
427 my $pkgfile = &$get_pkgfile($ver);
428 my $url;
429
430 die "changelog for '${pkgname}_$ver->{VerStr}' not available\n"
431 if !($pkgfile && ($url = &$get_changelog_url($pkgname, $info, $ver->{VerStr}, $pkgfile->{Origin}, $pkgfile->{Component})));
432
433 my $data = "";
434
435 my $pmg_cfg = PMG::Config->new();
436 my $proxy = $pmg_cfg->get('admin', 'http_proxy');
437
438 my $ua = LWP::UserAgent->new;
439 $ua->agent("PMG/1.0");
440 $ua->timeout(10);
441 $ua->max_size(1024*1024);
442 $ua->ssl_opts(verify_hostname => 0); # don't care for changelogs
443
444 if ($proxy) {
445 $ua->proxy(['http', 'https'], $proxy);
446 } else {
447 $ua->env_proxy;
448 }
449
450 my $username;
451 my $pw;
452
453 if ($pkgfile->{Origin} eq 'Proxmox' && $pkgfile->{Component} eq 'pmg-enterprise') {
454 my $info = PMG::API2::Subscription::read_etc_subscription();
455 if ($info->{status} eq 'active') {
456 $username = $info->{key};
457 $pw = PMG::Utils::get_hwaddress();
458 $ua->credentials("enterprise.proxmox.com:443", 'pmg-enterprise-repository',
459 $username, $pw);
460 }
461 }
462
463 syslog('info', "GET $url\n");
464 my $response = $ua->get($url);
465
466 if ($response->is_success) {
467 $data = $response->decoded_content;
468 } else {
469 PVE::Exception::raise($response->message, code => $response->code);
470 }
471
472 return $data;
473 }});
474
475 __PACKAGE__->register_method({
476 name => 'repositories',
477 path => 'repositories',
478 method => 'GET',
479 proxyto => 'node',
480 description => "Get APT repository information.",
481 permissions => { check => [ 'admin', 'audit' ] },
482 parameters => {
483 additionalProperties => 0,
484 properties => {
485 node => get_standard_option('pve-node'),
486 },
487 },
488 returns => {
489 type => "object",
490 description => "Result from parsing the APT repository files in /etc/apt/.",
491 properties => {
492 files => {
493 type => "array",
494 description => "List of parsed repository files.",
495 items => {
496 type => "object",
497 properties => {
498 path => {
499 type => "string",
500 description => "Path to the problematic file.",
501 },
502 'file-type' => {
503 type => "string",
504 enum => [ 'list', 'sources' ],
505 description => "Format of the file.",
506 },
507 repositories => {
508 type => "array",
509 description => "The parsed repositories.",
510 items => {
511 type => "object",
512 properties => {
513 Types => {
514 type => "array",
515 description => "List of package types.",
516 items => {
517 type => "string",
518 enum => [ 'deb', 'deb-src' ],
519 },
520 },
521 URIs => {
522 description => "List of repository URIs.",
523 type => "array",
524 items => {
525 type => "string",
526 },
527 },
528 Suites => {
529 type => "array",
530 description => "List of package distribuitions",
531 items => {
532 type => "string",
533 },
534 },
535 Components => {
536 type => "array",
537 description => "List of repository components",
538 optional => 1, # not present if suite is absolute
539 items => {
540 type => "string",
541 },
542 },
543 Options => {
544 type => "array",
545 description => "Additional options",
546 optional => 1,
547 items => {
548 type => "object",
549 properties => {
550 Key => {
551 type => "string",
552 },
553 Values => {
554 type => "array",
555 items => {
556 type => "string",
557 },
558 },
559 },
560 },
561 },
562 Comment => {
563 type => "string",
564 description => "Associated comment",
565 optional => 1,
566 },
567 FileType => {
568 type => "string",
569 enum => [ 'list', 'sources' ],
570 description => "Format of the defining file.",
571 },
572 Enabled => {
573 type => "boolean",
574 description => "Whether the repository is enabled or not",
575 },
576 },
577 },
578 },
579 digest => {
580 type => "array",
581 description => "Digest of the file as bytes.",
582 items => {
583 type => "integer",
584 },
585 },
586 },
587 },
588 },
589 errors => {
590 type => "array",
591 description => "List of problematic repository files.",
592 items => {
593 type => "object",
594 properties => {
595 path => {
596 type => "string",
597 description => "Path to the problematic file.",
598 },
599 error => {
600 type => "string",
601 description => "The error message",
602 },
603 },
604 },
605 },
606 digest => {
607 type => "string",
608 description => "Common digest of all files.",
609 },
610 infos => {
611 type => "array",
612 description => "Additional information/warnings for APT repositories.",
613 items => {
614 type => "object",
615 properties => {
616 path => {
617 type => "string",
618 description => "Path to the associated file.",
619 },
620 index => {
621 type => "string",
622 description => "Index of the associated repository within the file.",
623 },
624 property => {
625 type => "string",
626 description => "Property from which the info originates.",
627 optional => 1,
628 },
629 kind => {
630 type => "string",
631 description => "Kind of the information (e.g. warning).",
632 },
633 message => {
634 type => "string",
635 description => "Information message.",
636 }
637 },
638 },
639 },
640 'standard-repos' => {
641 type => "array",
642 description => "List of standard repositories and their configuration status",
643 items => {
644 type => "object",
645 properties => {
646 handle => {
647 type => "string",
648 description => "Handle to identify the repository.",
649 },
650 name => {
651 type => "string",
652 description => "Display name of the repository.",
653 },
654 description => {
655 type => "string",
656 description => "Description of the repository.",
657 },
658 status => {
659 type => "boolean",
660 optional => 1,
661 description => "Indicating enabled/disabled status, if the " .
662 "repository is configured.",
663 },
664 },
665 },
666 },
667 },
668 },
669 code => sub {
670 my ($param) = @_;
671
672 return Proxmox::RS::APT::Repositories::repositories("pmg");
673 }});
674
675 __PACKAGE__->register_method({
676 name => 'add_repository',
677 path => 'repositories',
678 method => 'PUT',
679 description => "Add a standard repository to the configuration",
680 permissions => { check => [ 'admin' ] },
681 protected => 1,
682 proxyto => 'node',
683 parameters => {
684 additionalProperties => 0,
685 properties => {
686 node => get_standard_option('pve-node'),
687 handle => {
688 type => 'string',
689 description => "Handle that identifies a repository.",
690 },
691 digest => {
692 type => "string",
693 description => "Digest to detect modifications.",
694 maxLength => 80,
695 optional => 1,
696 },
697 },
698 },
699 returns => {
700 type => 'null',
701 },
702 code => sub {
703 my ($param) = @_;
704
705 Proxmox::RS::APT::Repositories::add_repository($param->{handle}, "pmg", $param->{digest});
706 }});
707
708 __PACKAGE__->register_method({
709 name => 'change_repository',
710 path => 'repositories',
711 method => 'POST',
712 description => "Change the properties of a repository. Currently only allows enabling/disabling.",
713 permissions => { check => [ 'admin' ] },
714 protected => 1,
715 proxyto => 'node',
716 parameters => {
717 additionalProperties => 0,
718 properties => {
719 node => get_standard_option('pve-node'),
720 path => {
721 type => 'string',
722 description => "Path to the containing file.",
723 },
724 index => {
725 type => 'integer',
726 description => "Index within the file (starting from 0).",
727 },
728 enabled => {
729 type => 'boolean',
730 description => "Whether the repository should be enabled or not.",
731 optional => 1,
732 },
733 digest => {
734 type => "string",
735 description => "Digest to detect modifications.",
736 maxLength => 80,
737 optional => 1,
738 },
739 },
740 },
741 returns => {
742 type => 'null',
743 },
744 code => sub {
745 my ($param) = @_;
746
747 my $options = {};
748
749 my $enabled = $param->{enabled};
750 $options->{enabled} = int($enabled) if defined($enabled);
751
752 Proxmox::RS::APT::Repositories::change_repository(
753 $param->{path},
754 int($param->{index}),
755 $options,
756 $param->{digest}
757 );
758 }});
759
760 __PACKAGE__->register_method({
761 name => 'versions',
762 path => 'versions',
763 method => 'GET',
764 proxyto => 'node',
765 description => "Get package information for important Proxmox packages.",
766 permissions => { check => [ 'admin', 'audit' ] },
767 parameters => {
768 additionalProperties => 0,
769 properties => {
770 node => get_standard_option('pve-node'),
771 },
772 },
773 returns => {
774 type => "array",
775 items => {
776 type => "object",
777 properties => {},
778 },
779 },
780 code => sub {
781 my ($param) = @_;
782
783 my $cache = &$get_apt_cache();
784 my $policy = $cache->policy;
785 my $pkgrecords = $cache->packages();
786
787 # order most important things first
788 my @list = qw(proxmox-mailgateway pmg-api pmg-gui);
789
790 my $aptver = $AptPkg::System::_system->versioning();
791 my $byver = sub { $aptver->compare($cache->{$b}->{CurrentVer}->{VerStr}, $cache->{$a}->{CurrentVer}->{VerStr}) };
792 push @list, sort $byver grep { /^(?:pve|proxmox)-kernel-/ && $cache->{$_}->{CurrentState} eq 'Installed' } keys %$cache;
793
794 my @opt_pack = qw(
795 ifupdown
796 ifupdown2
797 libpve-apiclient-perl
798 proxmox-mailgateway-container
799 proxmox-offline-mirror-helper
800 pve-firmware
801 zfsutils-linux
802 );
803
804 my @pkgs = qw(
805 clamav-daemon
806 libarchive-perl
807 libjs-extjs
808 libjs-framework7
809 libpve-common-perl
810 libpve-http-server-perl
811 libproxmox-acme-perl
812 libproxmox-acme-plugins
813 libxdgmime-perl
814 lvm2
815 pmg-docs
816 pmg-i18n
817 pmg-log-tracker
818 postgresql-13
819 proxmox-mini-journalreader
820 proxmox-spamassassin
821 proxmox-widget-toolkit
822 pve-xtermjs
823 vncterm
824 );
825
826 push @list, (sort @pkgs, @opt_pack);
827
828 my (undef, undef, $kernel_release) = POSIX::uname();
829 my $pmgver = PMG::pmgcfg::version_text();
830
831 my $pkglist = [];
832 foreach my $pkgname (@list) {
833 my $p = $cache->{$pkgname};
834 my $info = $pkgrecords->lookup($pkgname);
835 my $candidate_ver = defined($p) ? $policy->candidate($p) : undef;
836 my $res;
837 if (my $current_ver = $p->{CurrentVer}) {
838 $res = &$assemble_pkginfo($pkgname, $info, $current_ver,
839 $candidate_ver || $current_ver);
840 } elsif ($candidate_ver) {
841 $res = &$assemble_pkginfo($pkgname, $info, $candidate_ver,
842 $candidate_ver);
843 delete $res->{OldVersion};
844 } else {
845 next;
846 }
847 $res->{CurrentState} = $p->{CurrentState};
848
849 if (grep( /^$pkgname$/, @opt_pack)) {
850 next if $res->{CurrentState} eq 'NotInstalled';
851 }
852
853 # hack: add some useful information (used by 'pmgversion -v')
854 if ($pkgname =~ /^proxmox-mailgateway(-container)?$/) {
855 $res->{ManagerVersion} = $pmgver;
856 $res->{RunningKernel} = $kernel_release;
857 if ($pkgname eq 'proxmox-mailgateway-container') {
858 # another hack: replace proxmox-mailgateway with CT meta pkg
859 shift @$pkglist;
860 unshift @$pkglist, $res;
861 next;
862 }
863 }
864
865 push @$pkglist, $res;
866 }
867
868 return $pkglist;
869 }});
870
871 1;