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