Handle hostnames with upper-case letters
[webmin.git] / package-updates / package-updates-lib.pl
1 # Functions for checking for updates to packages from YUM, APT or some other
2 # update system.
3
4 BEGIN { push(@INC, ".."); };
5 eval "use WebminCore;";
6 &init_config();
7 &foreign_require("software", "software-lib.pl");
8 &foreign_require("cron", "cron-lib.pl");
9 &foreign_require("webmin", "webmin-lib.pl");
10 use Data::Dumper;
11
12 $available_cache_file = "$module_config_directory/available.cache";
13 $current_cache_file = "$module_config_directory/current.cache";
14 $updates_cache_file = "$module_config_directory/updates.cache";
15 $cron_cmd = "$module_config_directory/update.pl";
16
17 $yum_cache_file = "$module_config_directory/yumcache";
18 $apt_cache_file = "$module_config_directory/aptcache";
19 $yum_changelog_cache_dir = "$module_config_directory/yumchangelog";
20
21 # get_software_packages()
22 # Fills in software::packages with list of installed packages (if missing),
23 # returns count.
24 sub get_software_packages
25 {
26 if (!$get_software_packages_cache) {
27         %software::packages = ( );
28         $get_software_packages_cache = &software::list_packages();
29         }
30 return $get_software_packages_cache;
31 }
32
33 # list_current(nocache)
34 # Returns a list of packages and versions for installed software. Keys are :
35 #  name - The my package name (ie. CSWapache2)
36 #  update - Name used to refer to it by the updates system (ie. apache2)
37 #  version - Version number
38 #  epoch - Epoch part of the version
39 #  desc - Human-readable description
40 sub list_current
41 {
42 my ($nocache) = @_;
43 if ($nocache || &cache_expired($current_cache_file)) {
44         my $n = &get_software_packages();
45         my @rv;
46         for(my $i=0; $i<$n; $i++) {
47                 push(@rv, { 'name' => $software::packages{$i,'name'},
48                             'update' => $software::packages{$i,'name'},
49                             'version' =>
50                               $software::packages{$i,'version'},
51                             'epoch' =>
52                               $software::packages{$i,'epoch'},
53                             'desc' =>
54                               $software::packages{$i,'desc'},
55                             'system' => $software::update_system,
56                         });
57                 &fix_pkgadd_version($rv[$#rv]);
58                 }
59
60         # Filter out dupes and sort by name
61         @rv = &filter_duplicates(\@rv);
62
63         &write_cache_file($current_cache_file, \@rv);
64         return @rv;
65         }
66 else {
67         return &read_cache_file($current_cache_file);
68         }
69 }
70
71 # list_available(nocache)
72 # Returns the names and versions of packages available from the update system
73 sub list_available
74 {
75 my ($nocache) = @_;
76 my $expired = &cache_expired($available_cache_file);
77 if ($nocache || $expired == 2 ||
78     $expired == 1 && !&check_available_lock()) {
79         # Get from update system
80         my @rv;
81         my @avail = &packages_available();
82         foreach my $avail (@avail) {
83                 $avail->{'update'} = $avail->{'name'};
84                 $avail->{'name'} = &csw_to_pkgadd($avail->{'name'});
85                 push(@rv, $avail);
86                 }
87
88         # Filter out dupes and sort by name
89         @rv = &filter_duplicates(\@rv);
90
91         if (!@rv) {
92                 # Failed .. fall back to cache
93                 @rv = &read_cache_file($available_cache_file);
94                 }
95         &write_cache_file($available_cache_file, \@rv);
96         return @rv;
97         }
98 else {
99         return &read_cache_file($available_cache_file);
100         }
101 }
102
103 # check_available_lock()
104 # Returns 1 if the package update system is currently locked
105 sub check_available_lock
106 {
107 if ($software::update_system eq "yum") {
108         return &check_pid_file("/var/run/yum.pid");
109         }
110 return 0;
111 }
112
113 # filter_duplicates(&packages)
114 # Given a list of package structures, orders them by name and version number,
115 # and removes dupes with the same name
116 sub filter_duplicates
117 {
118 my ($pkgs) = @_;
119 my @rv = sort { $a->{'name'} cmp $b->{'name'} ||
120                  &compare_versions($b, $a) } @$pkgs;
121 my %done;
122 return grep { !$done{$_->{'name'},$_->{'system'}}++ } @rv;
123 }
124
125 # cache_expired(file)
126 # Checks if some cache has expired. Returns 0 if OK, 1 if expired, 2 if
127 # totally missing.
128 sub cache_expired 
129 {
130 my ($file) = @_;
131 my @st = stat($file);
132 return 2 if (!@st);
133 if (!$config{'cache_time'} || time()-$st[9] > $config{'cache_time'}*60*60) {
134         return 1;
135         }
136 return 0;               
137 }                               
138
139 sub write_cache_file
140 {
141 my ($file, $arr) = @_;
142 &open_tempfile(FILE, ">$file");
143 &print_tempfile(FILE, Dumper($arr));
144 &close_tempfile(FILE);
145 $read_cache_file_cache{$file} = $arr;
146 }
147
148 # read_cache_file(file)
149 # Returns the contents of some cache file, as an array ref
150 sub read_cache_file
151 {
152 my ($file) = @_;
153 if (defined($read_cache_file_cache{$file})) {
154         return @{$read_cache_file_cache{$file}};
155         }
156 if (-r $file) {
157         do $file;
158         $read_cache_file_cache{$file} = $VAR1;
159         return @$VAR1;
160         }
161 return ( );
162 }
163
164 # compare_versions(&pkg1, &pk2)
165 # Returns -1 if the version of pkg1 is older than pkg2, 1 if newer, 0 if same.
166 sub compare_versions
167 {
168 my ($pkg1, $pkg2) = @_;
169 if ($pkg1->{'system'} eq 'webmin' && $pkg2->{'system'} eq 'webmin') {
170         # Webmin module version compares are always numeric
171         return $pkg1->{'version'} <=> $pkg2->{'version'};
172         }
173 my $ec = $pkg1->{'epoch'} <=> $pkg2->{'epoch'};
174 if ($ec && ($pkg1->{'epoch'} eq '' || $pkg2->{'epoch'} eq '') &&
175     $pkg1->{'system'} eq 'apt') {
176         # On some Debian systems, we don't have a my epoch
177         $ec = undef;
178         }
179 return $ec ||
180        &software::compare_versions($pkg1->{'version'}, $pkg2->{'version'});
181 }
182
183 sub find_cron_job
184 {
185 my @jobs = &cron::list_cron_jobs();
186 my ($job) = grep { $_->{'user'} eq 'root' &&
187                       $_->{'command'} eq $cron_cmd } @jobs;
188 return $job;
189 }
190
191 # packages_available()
192 # Returns a list of all available packages, as hash references with name and
193 # version keys. These come from the APT, YUM or CSW update system, if available.
194 # If not, nothing is returned.
195 sub packages_available
196 {
197 if (@packages_available_cache) {
198         return @packages_available_cache;
199         }
200 if (defined(&software::update_system_available)) {
201         # From a decent package system
202         my @rv = software::update_system_available();
203         my %done;
204         foreach my $p (@rv) {
205                 $p->{'system'} = $software::update_system;
206                 $p->{'version'} =~ s/,REV=.*//i;                # For CSW
207                 if ($p->{'system'} eq 'apt' && !$p->{'source'}) {
208                         $p->{'source'} =
209                             $p->{'file'} =~ /virtualmin/i ? 'virtualmin' : 
210                             $p->{'file'} =~ /debian/i ? 'debian' :
211                             $p->{'file'} =~ /ubuntu/i ? 'ubuntu' : undef;
212                         }
213                 $done{$p->{'name'}} = $p;
214                 }
215         if ($software::update_system eq "yum" &&
216             &has_command("up2date")) {
217                 # YUM is the package system select, but up2date is installed
218                 # too (ie. RHEL). Fetch its packages too..
219                 if (!$done_rhn_lib++) {
220                         do "../software/rhn-lib.pl";
221                         }
222                 my @rhnrv = &update_system_available();
223                 foreach my $p (@rhnrv) {
224                         $p->{'system'} = "rhn";
225                         my $d = $done{$p->{'name'}};
226                         if ($d) {
227                                 # Seen already .. but is this better?
228                                 if (&compare_versions($p, $d) > 0) {
229                                         # Yes .. replace
230                                         @rv = grep { $_ ne $d } @rv;
231                                         push(@rv, $p);
232                                         $done{$p->{'name'}} = $p;
233                                         }
234                                 }
235                         else {
236                                 push(@rv, $p);
237                                 $done{$p->{'name'}} = $p;
238                                 }
239                         }
240                 }
241         @packages_available_cache = @rv;
242         return @rv;
243         }
244 return ( );
245 }
246
247 sub supports_updates_available
248 {
249 return defined(&software::update_system_updates);
250 }
251
252 # updates_available(no-cache)
253 # Returns an array of hash refs of package updates available, according to
254 # the update system, with caching.
255 sub updates_available
256 {
257 my ($nocache) = @_;
258 if (!scalar(@updates_available_cache)) {
259         if ($nocache || &cache_expired($updates_cache_file)) {
260                 # Get from original source
261                 @updates_available_cache = &software::update_system_updates();
262                 foreach my $a (@updates_available_cache) {
263                         $a->{'update'} = $a->{'name'};
264                         $a->{'system'} = $software::update_system;
265                         }
266                 &write_cache_file($updates_cache_file,
267                                   \@updates_available_cache);
268                 }
269         else {
270                 # Use on-disk cache
271                 @updates_available_cache =
272                         &read_cache_file($updates_cache_file);
273                 }
274         }
275 return @updates_available_cache;
276 }
277
278 # package_install(package-name, [system])
279 # Install some package, either from an update system or from Webmin. Returns
280 # a list of updated package names.
281 sub package_install
282 {
283 my ($name, $system) = @_;
284 my @rv;
285 my $pkg;
286
287 # First get from list of updates
288 ($pkg) = grep { $_->{'update'} eq $name &&
289                 ($_->{'system'} eq $system || !$system) }
290               sort { &compare_versions($b, $a) }
291                    &list_possible_updates(0);
292 if (!$pkg) {
293         # Then try list of all available packages
294         ($pkg) = grep { $_->{'update'} eq $name &&
295                         ($_->{'system'} eq $system || !$system) }
296                       sort { &compare_versions($b, $a) }
297                            &list_available(0);
298         }
299 if (!$pkg) {
300         print &text('update_efindpkg', $name),"<p>\n";
301         return ( );
302         }
303 if (defined(&software::update_system_install)) {
304         # Using some update system, like YUM or APT
305         &clean_environment();
306         if ($software::update_system eq $pkg->{'system'}) {
307                 # Can use the default system
308                 @rv = &software::update_system_install($name, undef, 1);
309                 }
310         else {
311                 # Another update system exists!! Use it..
312                 if (!$done_rhn_lib++) {
313                         do "../software/$pkg->{'system'}-lib.pl";
314                         }
315                 if (!$done_rhn_text++) {
316                         %text = ( %text, %software::text );
317                         }
318                 @rv = &update_system_install($name, undef, 1);
319                 }
320         &reset_environment();
321         }
322 else {
323         &error("Don't know how to install package $pkg->{'name'} with type $pkg->{'type'}");
324         }
325 # Flush installed cache
326 unlink($current_cache_file);
327 return @rv;
328 }
329
330 # package_install_multiple(&package-names, system)
331 # Install multiple packages, either from an update system or from Webmin.
332 # Returns a list of updated package names.
333 sub package_install_multiple
334 {
335 my ($names, $system) = @_;
336 my @rv;
337 my $pkg;
338
339 if (defined(&software::update_system_install)) {
340         # Using some update system, like YUM or APT
341         &clean_environment();
342         if ($software::update_system eq $system) {
343                 # Can use the default system
344                 @rv = &software::update_system_install(
345                         join(" ", @$names), undef, 1);
346                 }
347         else {
348                 # Another update system exists!! Use it..
349                 if (!$done_rhn_lib++) {
350                         do "../software/$pkg->{'system'}-lib.pl";
351                         }
352                 if (!$done_rhn_text++) {
353                         %text = ( %text, %software::text );
354                         }
355                 @rv = &update_system_install(join(" ", @$names), undef, 1);
356                 }
357         &reset_environment();
358         }
359 else {
360         &error("Don't know how to install packages");
361         }
362 # Flush installed cache
363 unlink($current_cache_file);
364 return @rv;
365 }
366
367 # list_package_operations(package|packages, system)
368 # Given a package (or space-separate package list), returns a list of all
369 # dependencies that will be installed
370 sub list_package_operations
371 {
372 my ($name, $system) = @_;
373 if (defined(&software::update_system_operations)) {
374         my @rv = &software::update_system_operations($name);
375         foreach my $p (@rv) {
376                 $p->{'system'} = $system;
377                 }
378         return @rv;
379         }
380 return ( );
381 }
382
383 # list_possible_updates([nocache])
384 # Returns a list of updates that are available. Each element in the array
385 # is a hash ref containing a name, version, description and severity flag.
386 # Intended for calling from themes. Nocache 0=cache everything, 1=flush all
387 # caches, 2=flush only current
388 sub list_possible_updates
389 {
390 my ($nocache) = @_;
391 my @rv;
392 my @current = &list_current($nocache);
393 if (&supports_updates_available()) {
394         # Software module supplies a function that can list just packages
395         # that need updating
396         my %currentmap;
397         foreach my $c (@current) {
398                 $currentmap{$c->{'name'},$c->{'system'}} ||= $c;
399                 }
400         foreach my $a (&updates_available($nocache == 1)) {
401                 my $c = $currentmap{$a->{'name'},$a->{'system'}};
402                 next if (!$c);
403                 next if ($a->{'version'} eq $c->{'version'});
404                 push(@rv, { 'name' => $a->{'name'},
405                             'update' => $a->{'update'},
406                             'system' => $a->{'system'},
407                             'version' => $a->{'version'},
408                             'oldversion' => $c->{'version'},
409                             'epoch' => $a->{'epoch'},
410                             'oldepoch' => $c->{'epoch'},
411                             'security' => $a->{'security'},
412                             'source' => $a->{'source'},
413                             'desc' => $c->{'desc'} || $a->{'desc'} });
414                 }
415         }
416 else {
417         # Compute from current and available list
418         my @avail = &list_available($nocache == 1);
419         my %availmap;
420         foreach my $a (@avail) {
421                 my $oa = $availmap{$a->{'name'},$a->{'system'}};
422                 if (!$oa || &compare_versions($a, $oa) > 0) {
423                         $availmap{$a->{'name'},$a->{'system'}} = $a;
424                         }
425                 }
426         foreach my $c (sort { $a->{'name'} cmp $b->{'name'} } @current) {
427                 # Work out the status
428                 my $a = $availmap{$c->{'name'},$c->{'system'}};
429                 if ($a->{'version'} && &compare_versions($a, $c) > 0) {
430                         # A regular update is available
431                         push(@rv, { 'name' => $a->{'name'},
432                                     'update' => $a->{'update'},
433                                     'system' => $a->{'system'},
434                                     'version' => $a->{'version'},
435                                     'oldversion' => $c->{'version'},
436                                     'epoch' => $a->{'epoch'},
437                                     'oldepoch' => $c->{'epoch'},
438                                     'security' => $a->{'security'},
439                                     'source' => $a->{'source'},
440                                     'desc' => $c->{'desc'} || $a->{'desc'},
441                                     'severity' => 0 });
442                         }
443                 }
444         }
445 return @rv;
446 }
447
448 # list_possible_installs([nocache])
449 # Returns a list of packages that could be installed, but are not yet
450 sub list_possible_installs
451 {
452 my ($nocache) = @_;
453 my @rv;
454 my @current = &list_current($nocache);
455 my @avail = &list_available($nocache == 1);
456 foreach my $a (sort { $a->{'name'} cmp $b->{'name'} } @avail) {
457         ($c) = grep { $_->{'name'} eq $a->{'name'} &&
458                       $_->{'system'} eq $a->{'system'} } @current;
459         if (!$c && &installation_candiate($a)) {
460                 push(@rv, { 'name' => $a->{'name'},
461                             'update' => $a->{'update'},
462                             'system' => $a->{'system'},
463                             'version' => $a->{'version'},
464                             'epoch' => $a->{'epoch'},
465                             'desc' => $a->{'desc'},
466                             'severity' => 0 });
467                 }
468         }
469 return @rv;
470 }
471
472 # csw_to_pkgadd(package)
473 # On Solaris systems, convert a CSW package name like ap2_modphp5 to a
474 # real package name like CSWap2modphp5
475 sub csw_to_pkgadd
476 {
477 my ($pn) = @_;
478 if ($gconfig{'os_type'} eq 'solaris') {
479         $pn =~ s/[_\-]//g;
480         $pn = "CSW$pn";
481         }
482 return $pn;
483 }
484
485 # fix_pkgadd_version(&package)
486 # If this is Solaris and the package version is missing, we need to make 
487 # a separate pkginfo call to get it.
488 sub fix_pkgadd_version
489 {
490 my ($pkg) = @_;
491 if ($gconfig{'os_type'} eq 'solaris') {
492         if (!$pkg->{'version'}) {
493                 # Make an extra call to get the version
494                 my @pinfo = &software::package_info($pkg->{'name'});
495                 $pinfo[4] =~ s/,REV=.*//i;
496                 $pkg->{'version'} = $pinfo[4];
497                 }
498         else {
499                 # Trip off the REV=
500                 $pkg->{'version'} =~ s/,REV=.*//i;
501                 }
502         }
503 $pkg->{'desc'} =~ s/^\Q$pkg->{'update'}\E\s+\-\s+//;
504 }
505
506 # installation_candiate(&package)
507 # Returns 1 if some package can be installed, even when it currently isn't.
508 # Always true for now.
509 sub installation_candiate
510 {
511 my ($p) = @_;
512 return 1;
513 }
514
515 # clear_repository_cache()
516 # Clear any YUM or APT caches
517 sub clear_repository_cache
518 {
519 if ($software::update_system eq "yum") {
520         &execute_command("yum clean all");
521         }
522 elsif ($software::update_system eq "apt") {
523         &execute_command("apt-get update");
524         }
525 }
526
527 # split_epoch(version)
528 # Splits a version formatted like 5:x.yy into an epoch and real version
529 sub split_epoch
530 {
531 my ($ver) = @_;
532 if ($ver =~ /^(\d+):(\S+)$/) {
533         return ($1, $2);
534         }
535 return (undef, $ver);
536 }
537
538 # get_changelog(&pacakge)
539 # If possible, returns information about what has changed in some update
540 sub get_changelog
541 {
542 my ($pkg) = @_;
543 if ($pkg->{'system'} eq 'yum') {
544         # See if yum supports changelog
545         if (!defined($supports_yum_changelog)) {
546                 my $out = &backquote_command("yum -h 2>&1 </dev/null");
547                 $supports_yum_changelog = $out =~ /changelog/ ? 1 : 0;
548                 }
549         return undef if (!$supports_yum_changelog);
550
551         # Check if we have this info cached
552         my $cfile = $yum_changelog_cache_dir."/".
553                        $pkg->{'name'}."-".$pkg->{'version'};
554         my $cl = &read_file_contents($cfile);
555         if (!$cl) {
556                 # Run it for this package and version
557                 my $started = 0;
558                 &open_execute_command(YUMCL, "yum changelog all ".
559                                              quotemeta($pkg->{'name'}), 1, 1);
560                 while(<YUMCL>) {
561                         s/\r|\n//g;
562                         if (/^\Q$pkg->{'name'}-$pkg->{'version'}\E/) {
563                                 $started = 1;
564                                 }
565                         elsif (/^==========/ || /^changelog stats/) {
566                                 $started = 0;
567                                 }
568                         elsif ($started) {
569                                 $cl .= $_."\n";
570                                 }
571                         }
572                 close(YUMCL);
573
574                 # Save the cache
575                 if (!-d $yum_changelog_cache_dir) {
576                         &make_dir($yum_changelog_cache_dir, 0700);
577                         }
578                 &open_tempfile(CACHE, ">$cfile");
579                 &print_tempfile(CACHE, $cl);
580                 &close_tempfile(CACHE);
581                 }
582         return $cl;
583         }
584 return undef;
585 }
586
587 sub flush_package_caches
588 {
589 unlink($current_cache_file);
590 unlink($updates_cache_file);
591 unlink($available_cache_file);
592 unlink($available_cache_file.'0');
593 unlink($available_cache_file.'1');
594 @packages_available_cache = ( );
595 %read_cache_file_cache = ( );
596 }
597
598 # list_for_mode(mode, nocache)
599 # If not is 'updates' or 'security', return just updates. Othewise, return
600 # all available packages.
601 sub list_for_mode
602 {
603 my ($mode, $nocache) = @_;
604 return $mode eq 'updates' || $mode eq 'security' ?
605         &list_possible_updates($nocache) : &list_available($nocache);
606 }
607
608 1;
609