]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/APT.pm
api: apt: repos: avoid creating implicit default for enabled
[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 use PMG::RS::APT::Repositories;
24
25 use AptPkg::Cache;
26 use AptPkg::Version;
27 use AptPkg::PkgRecords;
28
29 my $get_apt_cache = sub {
30
31 my $apt_cache = AptPkg::Cache->new() || die "unable to initialize AptPkg::Cache\n";
32
33 return $apt_cache;
34 };
35
36 use base qw(PVE::RESTHandler);
37
38 __PACKAGE__->register_method({
39 name => 'index',
40 path => '',
41 method => 'GET',
42 description => "Directory index for apt (Advanced Package Tool).",
43 permissions => {
44 user => 'all',
45 },
46 parameters => {
47 additionalProperties => 0,
48 properties => {
49 node => get_standard_option('pve-node'),
50 },
51 },
52 returns => {
53 type => "array",
54 items => {
55 type => "object",
56 properties => {
57 id => { type => 'string' },
58 },
59 },
60 links => [ { rel => 'child', href => "{id}" } ],
61 },
62 code => sub {
63 my ($param) = @_;
64
65 my $res = [
66 { id => 'changelog' },
67 { id => 'repositories' },
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 'pmg-enterprise') {
101 $changelog_url = "https://enterprise.proxmox.com/debian/pmg/$base/" .
102 "${pkgname}_${pkgver}.changelog";
103 } else {
104 $changelog_url = "http://download.proxmox.com/debian/pmg/$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 $pmg_pkgstatus_fn = "/var/lib/pmg/pkgupdates";
148
149 my $read_cached_pkgstatus = sub {
150 my $data = [];
151 eval {
152 my $jsonstr = PVE::Tools::file_get_contents($pmg_pkgstatus_fn, 5*1024*1024);
153 $data = decode_json($jsonstr);
154 };
155 if (my $err = $@) {
156 warn "error reading cached package status in $pmg_pkgstatus_fn\n";
157 }
158 return $data;
159 };
160
161 my $update_pmg_pkgstatus = sub {
162
163 syslog('info', "update new package list: $pmg_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($pmg_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 protected => 1,
236 proxyto => 'node',
237 permissions => { check => [ 'admin', 'audit' ] },
238 parameters => {
239 additionalProperties => 0,
240 properties => {
241 node => get_standard_option('pve-node'),
242 },
243 },
244 returns => {
245 type => "array",
246 items => {
247 type => "object",
248 properties => {},
249 },
250 },
251 code => sub {
252 my ($param) = @_;
253
254 if (my $st1 = File::stat::stat($pmg_pkgstatus_fn)) {
255 my $st2 = File::stat::stat("/var/cache/apt/pkgcache.bin");
256 my $st3 = File::stat::stat("/var/lib/dpkg/status");
257
258 if ($st2 && $st3 && $st2->mtime <= $st1->mtime && $st3->mtime <= $st1->mtime) {
259 if (my $data = &$read_cached_pkgstatus()) {
260 return $data;
261 }
262 }
263 }
264
265 my $pkglist = &$update_pmg_pkgstatus();
266
267 return $pkglist;
268 }});
269
270 __PACKAGE__->register_method({
271 name => 'update_database',
272 path => 'update',
273 method => 'POST',
274 description => "This is used to resynchronize the package index files from their sources (apt-get update).",
275 protected => 1,
276 proxyto => 'node',
277 permissions => { check => [ 'admin' ] },
278 parameters => {
279 additionalProperties => 0,
280 properties => {
281 node => get_standard_option('pve-node'),
282 notify => {
283 type => 'boolean',
284 description => "Send notification mail about new packages (to email address specified for user 'root\@pam').",
285 optional => 1,
286 default => 0,
287 },
288 quiet => {
289 type => 'boolean',
290 description => "Only produces output suitable for logging, omitting progress indicators.",
291 optional => 1,
292 default => 0,
293 },
294 },
295 },
296 returns => {
297 type => 'string',
298 },
299 code => sub {
300 my ($param) = @_;
301
302 my $rpcenv = PMG::RESTEnvironment->get();
303
304 my $authuser = $rpcenv->get_user();
305
306 my $realcmd = sub {
307 my $upid = shift;
308
309 my $pmg_cfg = PMG::Config->new();
310
311 my $http_proxy = $pmg_cfg->get('admin', 'http_proxy');
312 my $aptconf = "// no proxy configured\n";
313 if ($http_proxy) {
314 $aptconf = "Acquire::http::Proxy \"${http_proxy}\";\n";
315 }
316 my $aptcfn = "/etc/apt/apt.conf.d/76pmgproxy";
317 PVE::Tools::file_set_contents($aptcfn, $aptconf);
318
319 my $cmd = ['apt-get', 'update'];
320
321 print "starting apt-get update\n" if !$param->{quiet};
322
323 if ($param->{quiet}) {
324 PVE::Tools::run_command($cmd, outfunc => sub {}, errfunc => sub {});
325 } else {
326 PVE::Tools::run_command($cmd);
327 }
328
329 my $pkglist = &$update_pmg_pkgstatus();
330
331 if ($param->{notify} && scalar(@$pkglist)) {
332
333 my $mailfrom = "root";
334
335 if (my $mailto = $pmg_cfg->get('admin', 'email', 1)) {
336
337 my $text .= "The following updates are available:\n\n";
338
339 my $count = 0;
340 foreach my $p (sort {$a->{Package} cmp $b->{Package} } @$pkglist) {
341 next if $p->{NotifyStatus} && $p->{NotifyStatus} eq $p->{Version};
342 $count++;
343 if ($p->{OldVersion}) {
344 $text .= "$p->{Package}: $p->{OldVersion} ==> $p->{Version}\n";
345 } else {
346 $text .= "$p->{Package}: $p->{Version} (new)\n";
347 }
348 }
349
350 return if !$count;
351
352 my $hostname = `hostname -f` || PVE::INotify::nodename();
353 chomp $hostname;
354
355 my $subject = "New software packages available ($hostname)";
356 PVE::Tools::sendmail($mailto, $subject, $text, undef,
357 $mailfrom, 'Proxmox Mail Gateway');
358
359 foreach my $pi (@$pkglist) {
360 $pi->{NotifyStatus} = $pi->{Version};
361 }
362
363 PVE::Tools::file_set_contents($pmg_pkgstatus_fn, encode_json($pkglist));
364 }
365 }
366
367 return;
368 };
369
370 return $rpcenv->fork_worker('aptupdate', undef, $authuser, $realcmd);
371
372 }});
373
374 __PACKAGE__->register_method({
375 name => 'changelog',
376 path => 'changelog',
377 method => 'GET',
378 description => "Get package changelogs.",
379 proxyto => 'node',
380 permissions => { check => [ 'admin', 'audit' ] },
381 parameters => {
382 additionalProperties => 0,
383 properties => {
384 node => get_standard_option('pve-node'),
385 name => {
386 description => "Package name.",
387 type => 'string',
388 },
389 version => {
390 description => "Package version.",
391 type => 'string',
392 optional => 1,
393 },
394 },
395 },
396 returns => {
397 type => "string",
398 },
399 code => sub {
400 my ($param) = @_;
401
402 my $pkgname = $param->{name};
403
404 my $cache = &$get_apt_cache();
405 my $policy = $cache->policy;
406 my $p = $cache->{$pkgname} || die "no such package '$pkgname'\n";
407 my $pkgrecords = $cache->packages();
408
409 my $ver;
410 if ($param->{version}) {
411 if (my $available = $p->{VersionList}) {
412 for my $v (@$available) {
413 if ($v->{VerStr} eq $param->{version}) {
414 $ver = $v;
415 last;
416 }
417 }
418 }
419 die "package '$pkgname' version '$param->{version}' is not available\n" if !$ver;
420 } else {
421 $ver = $policy->candidate($p) || die "no installation candidate for package '$pkgname'\n";
422 }
423
424 my $info = $pkgrecords->lookup($pkgname);
425
426 my $pkgfile = &$get_pkgfile($ver);
427 my $url;
428
429 die "changelog for '${pkgname}_$ver->{VerStr}' not available\n"
430 if !($pkgfile && ($url = &$get_changelog_url($pkgname, $info, $ver->{VerStr}, $pkgfile->{Origin}, $pkgfile->{Component})));
431
432 my $data = "";
433
434 my $pmg_cfg = PMG::Config->new();
435 my $proxy = $pmg_cfg->get('admin', 'http_proxy');
436
437 my $ua = LWP::UserAgent->new;
438 $ua->agent("PMG/1.0");
439 $ua->timeout(10);
440 $ua->max_size(1024*1024);
441 $ua->ssl_opts(verify_hostname => 0); # don't care for changelogs
442
443 if ($proxy) {
444 $ua->proxy(['http', 'https'], $proxy);
445 } else {
446 $ua->env_proxy;
447 }
448
449 my $username;
450 my $pw;
451
452 if ($pkgfile->{Origin} eq 'Proxmox' && $pkgfile->{Component} eq 'pmg-enterprise') {
453 my $info = PVE::INotify::read_file('subscription');
454 if ($info->{status} eq 'Active') {
455 $username = $info->{key};
456 $pw = PMG::Utils::get_hwaddress();
457 $ua->credentials("enterprise.proxmox.com:443", 'pmg-enterprise-repository',
458 $username, $pw);
459 }
460 }
461
462 syslog('info', "GET $url\n");
463 my $response = $ua->get($url);
464
465 if ($response->is_success) {
466 $data = $response->decoded_content;
467 } else {
468 PVE::Exception::raise($response->message, code => $response->code);
469 }
470
471 return $data;
472 }});
473
474 __PACKAGE__->register_method({
475 name => 'repositories',
476 path => 'repositories',
477 method => 'GET',
478 proxyto => 'node',
479 description => "Get APT repository information.",
480 permissions => {
481 check => ['perm', '/nodes/{node}', [ 'Sys.Audit' ]],
482 },
483 parameters => {
484 additionalProperties => 0,
485 properties => {
486 node => get_standard_option('pve-node'),
487 },
488 },
489 returns => {
490 type => "object",
491 description => "Result from parsing the APT repository files in /etc/apt/.",
492 properties => {
493 files => {
494 type => "array",
495 description => "List of parsed repository files.",
496 items => {
497 type => "object",
498 properties => {
499 path => {
500 type => "string",
501 description => "Path to the problematic file.",
502 },
503 'file-type' => {
504 type => "string",
505 enum => [ 'list', 'sources' ],
506 description => "Format of the file.",
507 },
508 repositories => {
509 type => "array",
510 description => "The parsed repositories.",
511 items => {
512 type => "object",
513 properties => {
514 Types => {
515 type => "array",
516 description => "List of package types.",
517 items => {
518 type => "string",
519 enum => [ 'deb', 'deb-src' ],
520 },
521 },
522 URIs => {
523 description => "List of repository URIs.",
524 type => "array",
525 items => {
526 type => "string",
527 },
528 },
529 Suites => {
530 type => "array",
531 description => "List of package distribuitions",
532 items => {
533 type => "string",
534 },
535 },
536 Components => {
537 type => "array",
538 description => "List of repository components",
539 optional => 1, # not present if suite is absolute
540 items => {
541 type => "string",
542 },
543 },
544 Options => {
545 type => "array",
546 description => "Additional options",
547 optional => 1,
548 items => {
549 type => "object",
550 properties => {
551 Key => {
552 type => "string",
553 },
554 Values => {
555 type => "array",
556 items => {
557 type => "string",
558 },
559 },
560 },
561 },
562 },
563 Comment => {
564 type => "string",
565 description => "Associated comment",
566 optional => 1,
567 },
568 FileType => {
569 type => "string",
570 enum => [ 'list', 'sources' ],
571 description => "Format of the defining file.",
572 },
573 Enabled => {
574 type => "boolean",
575 description => "Whether the repository is enabled or not",
576 },
577 },
578 },
579 },
580 digest => {
581 type => "array",
582 description => "Digest of the file as bytes.",
583 items => {
584 type => "integer",
585 },
586 },
587 },
588 },
589 },
590 errors => {
591 type => "array",
592 description => "List of problematic repository files.",
593 items => {
594 type => "object",
595 properties => {
596 path => {
597 type => "string",
598 description => "Path to the problematic file.",
599 },
600 error => {
601 type => "string",
602 description => "The error message",
603 },
604 },
605 },
606 },
607 digest => {
608 type => "string",
609 description => "Common digest of all files.",
610 },
611 infos => {
612 type => "array",
613 description => "Additional information/warnings for APT repositories.",
614 items => {
615 type => "object",
616 properties => {
617 path => {
618 type => "string",
619 description => "Path to the associated file.",
620 },
621 index => {
622 type => "string",
623 description => "Index of the associated repository within the file.",
624 },
625 property => {
626 type => "string",
627 description => "Property from which the info originates.",
628 optional => 1,
629 },
630 kind => {
631 type => "string",
632 description => "Kind of the information (e.g. warning).",
633 },
634 message => {
635 type => "string",
636 description => "Information message.",
637 }
638 },
639 },
640 },
641 'standard-repos' => {
642 type => "array",
643 description => "List of standard repositories and their configuration status",
644 items => {
645 type => "object",
646 properties => {
647 handle => {
648 type => "string",
649 description => "Handle to identify the repository.",
650 },
651 name => {
652 type => "string",
653 description => "Display name of the repository.",
654 },
655 description => {
656 type => "string",
657 description => "Description of the repository.",
658 },
659 status => {
660 type => "boolean",
661 optional => 1,
662 description => "Indicating enabled/disabled status, if the " .
663 "repository is configured.",
664 },
665 },
666 },
667 },
668 },
669 },
670 code => sub {
671 my ($param) = @_;
672
673 return PMG::RS::APT::Repositories::repositories();
674 }});
675
676 __PACKAGE__->register_method({
677 name => 'add_repository',
678 path => 'repositories',
679 method => 'PUT',
680 description => "Add a standard repository to the configuration",
681 permissions => {
682 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
683 },
684 protected => 1,
685 proxyto => 'node',
686 parameters => {
687 additionalProperties => 0,
688 properties => {
689 node => get_standard_option('pve-node'),
690 handle => {
691 type => 'string',
692 description => "Handle that identifies a repository.",
693 },
694 digest => {
695 type => "string",
696 description => "Digest to detect modifications.",
697 maxLength => 80,
698 optional => 1,
699 },
700 },
701 },
702 returns => {
703 type => 'null',
704 },
705 code => sub {
706 my ($param) = @_;
707
708 PMG::RS::APT::Repositories::add_repository($param->{handle}, $param->{digest});
709 }});
710
711 __PACKAGE__->register_method({
712 name => 'change_repository',
713 path => 'repositories',
714 method => 'POST',
715 description => "Change the properties of a repository. Currently only allows enabling/disabling.",
716 permissions => {
717 check => ['perm', '/nodes/{node}', [ 'Sys.Modify' ]],
718 },
719 protected => 1,
720 proxyto => 'node',
721 parameters => {
722 additionalProperties => 0,
723 properties => {
724 node => get_standard_option('pve-node'),
725 path => {
726 type => 'string',
727 description => "Path to the containing file.",
728 },
729 index => {
730 type => 'integer',
731 description => "Index within the file (starting from 0).",
732 },
733 enabled => {
734 type => 'boolean',
735 description => "Whether the repository should be enabled or not.",
736 optional => 1,
737 },
738 digest => {
739 type => "string",
740 description => "Digest to detect modifications.",
741 maxLength => 80,
742 optional => 1,
743 },
744 },
745 },
746 returns => {
747 type => 'null',
748 },
749 code => sub {
750 my ($param) = @_;
751
752 my $options = {};
753
754 my $enabled = $param->{enabled};
755 $options->{enabled} = int($enabled) if defined($enabled);
756
757 PMG::RS::APT::Repositories::change_repository(
758 $param->{path},
759 int($param->{index}),
760 $options,
761 $param->{digest}
762 );
763 }});
764
765 __PACKAGE__->register_method({
766 name => 'versions',
767 path => 'versions',
768 method => 'GET',
769 proxyto => 'node',
770 description => "Get package information for important Proxmox packages.",
771 permissions => { check => [ 'admin', 'audit' ] },
772 parameters => {
773 additionalProperties => 0,
774 properties => {
775 node => get_standard_option('pve-node'),
776 },
777 },
778 returns => {
779 type => "array",
780 items => {
781 type => "object",
782 properties => {},
783 },
784 },
785 code => sub {
786 my ($param) = @_;
787
788 my $cache = &$get_apt_cache();
789 my $policy = $cache->policy;
790 my $pkgrecords = $cache->packages();
791
792 # order most important things first
793 my @list = qw(proxmox-mailgateway pmg-api pmg-gui);
794
795 my $aptver = $AptPkg::System::_system->versioning();
796 my $byver = sub { $aptver->compare($cache->{$b}->{CurrentVer}->{VerStr}, $cache->{$a}->{CurrentVer}->{VerStr}) };
797 push @list, sort $byver grep { /^pve-kernel-/ && $cache->{$_}->{CurrentState} eq 'Installed' } keys %$cache;
798
799 my @opt_pack = qw(
800 ifupdown
801 ifupdown2
802 libpve-apiclient-perl
803 proxmox-mailgateway-container
804 pve-firmware
805 zfsutils-linux
806 );
807
808 my @pkgs = qw(
809 clamav-daemon
810 libarchive-perl
811 libjs-extjs
812 libjs-framework7
813 libpve-common-perl
814 libpve-http-server-perl
815 libproxmox-acme-perl
816 libproxmox-acme-plugins
817 libxdgmime-perl
818 lvm2
819 pmg-docs
820 pmg-i18n
821 pmg-log-tracker
822 postgresql-13
823 proxmox-mini-journalreader
824 proxmox-spamassassin
825 proxmox-widget-toolkit
826 pve-xtermjs
827 vncterm
828 );
829
830 push @list, (sort @pkgs, @opt_pack);
831
832 my (undef, undef, $kernel_release) = POSIX::uname();
833 my $pmgver = PMG::pmgcfg::version_text();
834
835 my $pkglist = [];
836 foreach my $pkgname (@list) {
837 my $p = $cache->{$pkgname};
838 my $info = $pkgrecords->lookup($pkgname);
839 my $candidate_ver = defined($p) ? $policy->candidate($p) : undef;
840 my $res;
841 if (my $current_ver = $p->{CurrentVer}) {
842 $res = &$assemble_pkginfo($pkgname, $info, $current_ver,
843 $candidate_ver || $current_ver);
844 } elsif ($candidate_ver) {
845 $res = &$assemble_pkginfo($pkgname, $info, $candidate_ver,
846 $candidate_ver);
847 delete $res->{OldVersion};
848 } else {
849 next;
850 }
851 $res->{CurrentState} = $p->{CurrentState};
852
853 if (grep( /^$pkgname$/, @opt_pack)) {
854 next if $res->{CurrentState} eq 'NotInstalled';
855 }
856
857 # hack: add some useful information (used by 'pmgversion -v')
858 if ($pkgname =~ /^proxmox-mailgateway(-container)?$/) {
859 $res->{ManagerVersion} = $pmgver;
860 $res->{RunningKernel} = $kernel_release;
861 if ($pkgname eq 'proxmox-mailgateway-container') {
862 # another hack: replace proxmox-mailgateway with CT meta pkg
863 shift @$pkglist;
864 unshift @$pkglist, $res;
865 next;
866 }
867 }
868
869 push @$pkglist, $res;
870 }
871
872 return $pkglist;
873 }});
874
875 1;