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