1: #!/usr/bin/perl
2: #
3: # check-rpms, version 2.1.0
4: # Martin Siegert, SFU, siegert@sfu.ca, Feb 02
5: #
6: # ************ WARNING *****************************************************
7: # THIS PROGRAM IS PROVIDED "AS IS" WITHOUT
8: # WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLICIT.
9: # IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
10: # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
11: # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
12: # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
13: # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
14: # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
15: # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
16: # SUCH DAMAGE.
17: # **************************************************************************
18:
19: # check-rpms.pl is free software; you can redistribute it and/or modify
20: # it under the terms of the GNU General Public License as published by
21: # the Free Software Foundation; either version 2 of the License, or
22: # (at your option) any later version.
23: #
24: # check-rpms.pl is distributed in the hope that it will be useful,
25: # but WITHOUT ANY WARRANTY; without even the implied warranty of
26: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
27: # GNU General Public License for more details:
28: # http://www.gnu.org/licenses/gpl.html
29:
30: use Getopt::Long;
31:
32: my $retval = &GetOptions("verbose|v","lm|list-missing","lq|list-questionable",
33: "dir|d=s","ftp:s","noftp","download|dl","recheck|r",
34: "nk|no-kernel","update","c=s");
35:
36: if ( $retval == 0 ) {
37: usage();
38: }
39:
40: # executables
41: $FTPLS = "ncftpls";
42: $FTPGET = "ncftpget";
43: $GREP = "grep";
44:
45: # default values
46: $RHversion = (split /\s/, `cat /etc/redhat-release`)[4];
47: $DEFCONF = "/usr/local/etc/check-rpms.conf";
48: $DEFRPMDIR = "/mnt/redhat/RedHat/RPMS";
49: $DEFFTPSERVER = "updates.redhat.com";
50: $DEFFTPUPDATES = "$RHversion/en/os";
51: $DEFRPMUSER = "nobody";
52:
53: $RPMDIR=$DEFRPMDIR;
54:
55: # configuration
56: # the configuration file should set the $RPMDIR variable and/or $FTPSERVER,
57: # $FTPUPDATES and $DOWNLOADDIR variables, and the $RPMUSER variable.
58: if ($opt_c) {
59: $CONF = $opt_c;
60: } else {
61: $CONF = $DEFCONF;
62: }
63:
64: if ( -f $CONF) {
65: require($CONF);
66: } else {
67: $FTPSERVER = $DEFFTPSERVER;
68: $FTPUPDATES = $DEFFTPUPDATES;
69: }
70:
71: # check whether we are running as root
72: if ($< == 0){
73: if (! $RPMUSER) {
74: $RPMUSER = $DEFRPMUSER;
75: }
76: $RPMUID = getpwnam($RPMUSER);
77: if (! $RPMUID) {
78: die "You do not seem to have a $RPMUSER user on your system.\nSet the \$RPMUSER variable in the $CONF configuration file to a non-root user.\n";
79: }
80: if ($RPMUID == 0) {
81: die "You must set the \$RPMUSER variable in $CONF to a non-root user.\n";
82: }
83: # switch to $RPMUID
84: $> = $RPMUID;
85: if ($> != $RPMUID) { die "switching to $RPMUID uid failed.\n" }
86: }
87:
88: # command-line arguments
89: $verbose = $opt_verbose;
90: $list_missing = $opt_lm;
91: $questionable = $opt_lq;
92: $no_kernel = $opt_nk;
93: $download = $opt_download;
94: $recheck = $opt_recheck;
95: $update = $opt_update;
96:
97: if (defined $opt_update && $< != 0) {
98: die "You must be root in order to update rpms.\n";
99: }
100:
101: if ( defined $opt_dir ){
102: $RPMDIR = $opt_dir;
103: }
104:
105: if (defined $opt_ftp && defined $opt_noftp) {
106: die "Setting -ftp and -noftp does not make sense, does it?\n";
107: }
108:
109: if (defined $opt_noftp) { $FTP = 0; }
110:
111: if (defined $opt_ftp || $FTP) {
112: $ftp = 1;
113: if ( $opt_ftp ) {
114: $_ = $opt_ftp;
115: ($FTPSERVER, $FTPUPDATES) = m/^([^\/]+)\/(.*)$/;
116: } elsif ( ! ($FTPSERVER && $FTPUPDATES)) {
117: $FTPSERVER = $DEFFTPSERVER;
118: $FTPUPDATES = $DEFFTPUPDATES;
119: }
120:
121: if (defined $opt_update){
122: $download=1;
123: }
124:
125: if ($download || $recheck) {
126: if ( ! -d $RPMDIR) {
127: $retval = system("mkdir -p $RPMDIR; chmod 700 $RPMDIR");
128: if ($retval) { die "error: could not create $RPMDIR\n"; }
129: }
130: }
131: } elsif ( (! -d $RPMDIR) || system("ls $RPMDIR/*.rpm > /dev/null 2>&1")) {
132: die "Either $RPMDIR does not exist or it does not contain any packages.\n";
133: }
134:
135: if ($recheck) {
136: $questionable=1;
137: }
138:
139: if (defined $opt_update || defined $opt_nk) {
140: $no_kernel=1;
141: }
142:
143: $PROC = `grep -i athlon /proc/cpuinfo`;
144: if ( ! "$PROC" ) {
145: $PROC = `uname -m`;
146: chomp($PROC);
147: } else {
148: $PROC = "athlon";
149: }
150:
151: @ARCHITECTURES = ("noarch", "i386", "i586", "i686");
152: if ( $RHversion > 7.0 ){
153: push(@ARCHITECTURES, "athlon");
154: }
155:
156: # get the local list of installed packages
157:
158: if ($verbose) {
159: print "updates for $PROC processor, RH $RHversion\n";
160: print "Getting list of installed packages\n";
161: }
162:
163: if ($< == 0) {
164: @local_rpm_list = `su $RPMUSER -c 'rpm -qa'`;
165: } else {
166: @local_rpm_list = `rpm -qa`;
167: }
168: chop(@local_rpm_list);
169:
170: %local_rpm = %remote_rpm = ();
171:
172: for (@local_rpm_list) {
173: # good place to test the regular expressions...
174: # ($pkg, $ver, $release) = m/^(.*)-([^-]*)-([^-]+)/;
175: # print "$_\t->$pkg, $ver, $release\n";
176:
177: my ($pkg, $pver) = m/([^ ]*)-([^-]+-[^-]+)/;
178: $local_rpm{$pkg} = $pver;
179: }
180:
181: # now connect to the remote host
182:
183: my @templist;
184: if ($ftp) {
185: if ( `rpm -q ncftp --pipe "grep 'not installed'"` ) {
186: die "you must have the ncftp package installed in order to use a\n",
187: "ftp server with check-rpms.\n";
188: }
189: $SOURCE = $FTPSERVER;
190: for (@ARCHITECTURES) {
191: my $FTPDIR = "$FTPUPDATES/$_";
192: if ($verbose) {
193: print ("Getting package lists from $FTPSERVER/$FTPDIR ...\n");
194: }
195: push(@templist, grep(/\.rpm$/, `$FTPLS -x "-1a" "ftp://$FTPSERVER/$FTPDIR/"`));
196: if ($?) { print STDERR "$FTPLS failed with status ",$?/256,".\n"; }
197: }
198: } else {
199: $SOURCE = $RPMDIR;
200: if ($verbose) {
201: print ("Getting package lists from $RPMDIR ...\n");
202: }
203: @templist = grep(/\.rpm$/, `(cd $RPMDIR;ls -1)`);
204: }
205:
206: #
207: # If two versions of the same RPM appear with different architectures
208: # and/or different versions, the right one must be found.
209: #
210:
211: $giveup = 0;
212: for (@templist) {
213: ($rpm, $pkg, $pver, $arch) = m/(([^ ]*)-([^- ]+-[^-]+\.(\w+)\.rpm))/;
214: if ($remote_rpm{$pkg}) {
215: # problem: there are several versions of the same package.
216: # this means that the package exists for different architectures
217: # (e.g., kernel, glibc, etc.) and/or that the remote server
218: # has several versions of the same package in which case the
219: # latest version must be picked.
220: my ($pkg1) = ($remote_rpm{$pkg} =~ m/([^-]+-[^-]+)\.\w+.rpm/);
221: my ($pkg2) = ($pver =~ m/([^-]+-[^-]+)\.\w+.rpm/);
222: my ($vcmp, $qflag) = cmp_versions($pkg1, $pkg2);
223: if ($qflag && $questionable) {
224: # cannot decide which of the two is newer - what should we do?
225: # print a warning that lists the two rpms.
226: # If running with --update, both packages must be rechecked with
227: # rpm -qp --queryformat '%{SERIAL}' <pkg>
228: if ($recheck || $update) {
229: my $decision = pkg_compare("$pkg-$remote_rpm{$pkg}",$rpm, $vcmp);
230: if ($decision < 0) {
231: # an error in the ftp download routine accured: giveup
232: $remote_rpm{$pkg} = undef;
233: $giveup = 1;
234: } elsif ($decision > 0) {
235: # second package is newer
236: $remote_rpm{$pkg} = $pver;
237: }
238: next;
239: } else {
240: mulpkg_msg("$pkg-$remote_rpm{$pkg}", $rpm, $vcmp);
241: print "** check whether this is correct or rerun with --recheck option.\n";
242: if ($vcmp < 0) {
243: $remote_rpm{$pkg} = $pver;
244: }
245: }
246: }
247: if ($vcmp == 0) {
248: # versions are equal: must be different architecture
249: # procedure to select the correct architecture:
250: # if $PROC = athlon: if available use $arch = athlon (exist for
251: # RH 7.1 or newer) otherwise use i686
252: # if $PROC = ix86: choose pkg with $PROC cmp $arch >= 0 and
253: # $arch cmp $prev_arch = 1
254: $_ = $remote_rpm{$pkg};
255: ($prev_arch) = m/.*\.(\w+)\.rpm$/;
256: if (cmp_arch($arch,$prev_arch)) { $remote_rpm{$pkg} = $pver };
257: } elsif ($vcmp < 0) { # second rpm is newer
258: $remote_rpm{$pkg} = $pver;
259: }
260: } else {
261: $remote_rpm{$pkg} = $pver;
262: }
263: }
264:
265: if ($giveup && defined $opt_update) {
266: die "Multiple versions of the same package were found on the server.\n",
267: "However, due to ftp download problems it could not be verified\n",
268: "which of the packages are the most recent ones.\n",
269: "If the choices specified above appear to be correct, rerun check-rpms\n",
270: "without the -lq (or --list-questionable) option. Otherwise, fix the download\n",
271: "problems or install those packages separately first.\n";
272: }
273:
274: #
275: # check for UPDated and DIFferent packages...
276: #
277:
278: for (@local_rpm_list) {
279: my ($pkg, $version) = m/^([^ ]*)-([^-]+-[^-]+)$/;
280: if (! $pkg) { print "Couldn't parse $_\n"; next; }
281: if ($no_kernel) {
282: if ($pkg eq 'kernel' || $pkg eq 'kernel-smp'
283: || $pkg eq 'kernel-enterprise' || $pkg eq 'kernel-BOOT'
284: || $pkg eq 'kernel-debug') { next; }
285: }
286: if (defined $remote_rpm{$pkg}) {
287: # this package has an update
288: my ($rversion) = ($remote_rpm{$pkg} =~ m/([^-]+-[^-]+)\.\w+.rpm/);
289: my $rpm = ($pkg . '-' . $remote_rpm{$pkg});
290: my ($vcmp,$qflag) = cmp_versions($version, $rversion);
291: if ( $qflag && $questionable ) {
292: # at least one of the version strings contains letters
293: push(@q_updates, $rpm);
294: } elsif ( $vcmp < 0 ) {
295: # local version is lower
296: if ( $qflag ) {
297: push(@q_updates, $rpm);
298: } else {
299: push(@updates, $rpm);
300: }
301: }
302: } elsif ($list_missing) {
303: print "Package '$pkg' missing from remote repository\n";
304: }
305: }
306:
307: if ($recheck && @q_updates) {
308: if ($ftp) {
309: for (@q_updates) {
310: ($arch) = m/[^ ]*-[^- ]+-[^-]+\.(\w+)\.rpm/;
311: push(@ftp_files, "$FTPUPDATES/$arch/$_");
312: }
313: if ($verbose) {
314: print "Getting questionable packages form $FTPSERVER ...\n";
315: }
316: my $status = system("$FTPGET $FTPSERVER $RPMDIR @ftp_files");
317: if ($status) {
318: if ($< == 0) {
319: # if we are running as root exit to avoid symlink attacks, etc.
320: die "$FTPGET failed with status ", $status/256, ".\n";
321: } else {
322: print STDERR "warning: $FTPGET failed with status ", $status/256, ".\n";
323: }
324: }
325: }
326: for (@q_updates) {
327: if ($verbose) {print "** rechecking $_ ... ";}
328: my $errmsg = `rpm -Uvh --test --nodeps --pipe 'grep -v ^Preparing' $RPMDIR/$_ 2>&1`;
329: if (! $errmsg) {
330: # no error message, i.e., the rpm is needed.
331: push(@updates,$_);
332: if ($verbose) {print "needed!\n";}
333: } elsif ($verbose) {
334: print "not needed:\n$errmsg\n";
335: }
336: }
337: @q_updates=();
338: }
339:
340: #
341: # print list of new files and download ...
342: #
343:
344: @updates = sort @updates;
345: if (@updates) {
346: if ($verbose) {
347: print "\nRPM files to be updated:\n\n";
348: }
349: for (@updates) {
350: print "$_\n";
351: }
352: if ($download) {
353: @ftp_files=();
354: for (@updates) {
355: ($arch) = m/[^ ]*-[^- ]+-[^-]+\.(\w+)\.rpm/;
356: push(@ftp_files, "$FTPUPDATES/$arch/$_");
357: }
358: if ($verbose) {
359: print "starting downloads ... \n";
360: }
361: my $status = system("$FTPGET $FTPSERVER $RPMDIR @ftp_files");
362: if ($status) {
363: if ($< == 0) {
364: # if we are running as root exit to avoid symlink attacks, etc.
365: die "$FTPGET failed with status ", $status/256, ".\n";
366: } else {
367: print STDERR "warning: $FTPGET failed with status ", $status/256, ".\n";
368: }
369: } elsif ($verbose) {
370: print "... done.\n";
371: }
372: }
373: }
374:
375: @q_updates = sort @q_updates;
376: if (@q_updates && $questionable) {
377: if ($verbose) {
378: print "\nRPM files that may need to be updated:\n\n";
379: for (@q_updates) {
380: my ($old) = m/^([^ ]*)-[^-]+-[^-]+\.\w+\.rpm$/;
381: $old = `rpm -q $old`;
382: chomp($old);
383: print "upgrade ", $old, " to ", $_, " ?\n";
384: }
385: } else {
386: for (@q_updates) {
387: print "$_\n";
388: }
389: }
390: if ($download) {
391: @ftp_files=();
392: for (@updates) {
393: ($arch) = m/[^ ]*-[^- ]+-[^-]+\.(\w+)\.rpm/;
394: push(@ftp_files, $FTPUPDATES/$arch/$_);
395: }
396: if ($verbose) {
397: print "starting downloads ... \n";
398: system("$FTPGET $FTPSERVER $$RPMDIR @ftp_files");
399: print "... done.\n";
400: } else {
401: system("$FTPGET $FTPSERVER $$RPMDIR @ftp_files");
402: }
403: }
404: }
405:
406: if ($verbose && !(@updates || @q_updates)) {
407: print "No new updates are available in $SOURCE\n";
408: }
409:
410: if ($opt_update) {
411: if (@q_updates){
412: push(@updates,@q_updates);
413: }
414: if (@updates) {
415: if ($verbose) {
416: print "Running rpm -Fvh ...\n";
417: }
418: # switch to UID=0
419: $> = $<;
420: system("(cd $RPMDIR;rpm -Fvh @updates)");
421: }
422: }
423:
424: # download routine
425: sub ftp_download {
426: my ($FTPSERVER, $FTPDIR, $downloaddir, @packages) = @_;
427: my @ftp_packages=();
428: for (@packages) {
429: my ($arch) = m/[^ ]*-[^-]+-[^-]*\.(\w+)\.rpm$/;
430: push(@ftp_packages,"$FTPDIR/$arch/$_");
431: }
432: my $status = system("$FTPGET $FTPSERVER $downloaddir @ftp_packages");
433: return $status;
434: }
435:
436: sub pkg_compare($$$) {
437: my ($pkg1, $pkg2, $cmp) = @_;
438: if (defined $opt_ftp) {
439: if ($verbose) {
440: my ($pkg) = ($pkg1 =~ /([^ ]*)-[^-]+-[^-]+\.\w+\.rpm/);
441: print "The ftp server provides multiple versions of the $pkg package.\n",
442: "Downloading $pkg1 and $pkg2 in order to find out which is newer.\n";
443: }
444: my $status = ftp_download($FTPSERVER, $FTPUPDATES, $RPMDIR, ($pkg1, $pkg2));
445: if ($status) {
446: # at this point just give up ...
447: print STDERR "** $FTPGET failed with status ", $status/256, ".\n";
448: mulpkg_msg($pkg1, $pkg2, $cmp);
449: return -1;
450: }
451: }
452: my $serial1 = `rpm -qp --queryformat '%{SERIAL}' $RPMDIR/$pkg1`;
453: my $serial2 = `rpm -qp --queryformat '%{SERIAL}' $RPMDIR/$pkg2`;
454: ($serial2 > $serial1) ? return 1 : return 0;
455: }
456:
457: sub mulpkg_msg($$$) {
458: my ($pkg1, $pkg2, $cmp) = @_;
459: print "** The server provides two versions of the same package:\n",
460: "** $pkg1 and $pkg2.\n";
461: if ($cmp > 0) {
462: print "** It appears that $pkg-$remote_rpm{$pkg} is newer.\n"
463: } else {
464: print "** It appears that $pkg-$pver is newer.\n";
465: }
466: }
467:
468: #############################################################################
469: #
470: # Version comparison utilities
471: #
472:
473: sub hack_version($) {
474: my ($pver) = @_;
475: $pver =~ s/(\d+)/sprintf("%08d", $1)/eg; # pad numbers with leading zeros to make alphabetical sort do the right thing
476: $pver = (sprintf "%-80s", $pver); # pad with spaces so that "3.2.1" is greater than "3.2"
477: return $pver;
478: }
479:
480: sub cmp_versions($$) {
481: my ($pkg1, $pkg2) = @_;
482:
483: # shortcut if they're obviously the same.
484: return (0,0) if ($pkg1 eq $pkg2);
485:
486: # split into version and release
487: my ($ver1, $rel1) = ($pkg1 =~ m/([^-]+)-([^-]+)/);
488: my ($ver2, $rel2) = ($pkg2 =~ m/([^-]+)-([^-]+)/);
489:
490: if ($ver1 ne $ver2) {
491: my $qflag = ((grep /[A-z]/, $ver1) || (grep /[A-z]/, $ver2));
492: $ver1 = hack_version($ver1);
493: $ver2 = hack_version($ver2);
494: return ($ver1 cmp $ver2, $qflag);
495: } else {
496: my $qflag = ((grep /[A-z]/, $rel1) || (grep /[A-z]/, $rel2));
497: $rel1 = hack_version($rel1);
498: $rel2 = hack_version($rel2);
499: return ($rel1 cmp $rel2, $qflag);
500: }
501: }
502:
503: sub cmp_arch($$) {
504: my ($arch1, $arch2) = @_;
505: my $retval = 0;
506: $archcmp = ($arch1 cmp $arch2) > 0;
507: if ( "$PROC" eq "athlon" ) {
508: if ( "$arch2" ne "athlon"
509: && ( "$arch1" eq "athlon" || $archcmp )){
510: $retval = 1;
511: }
512: } elsif ( $archcmp && ($PROC cmp $arch1) >= 0 ) {
513: $retval = 1;
514: }
515: return $retval;
516: }
517:
518: # @tests = ('3.2', '3.2',
519: # '3.2a', '3.2a',
520: # '3.2', '3.2a',
521: # '3.2', '3.3',
522: # '3.2', '3.2.1',
523: # '1.2.5i', '1.2.5.1',
524: # '1.6.3p6', '1.6.4');
525: #
526: # while (@tests) {
527: # $a = shift(@tests);
528: # $b = shift(@tests);
529: # printf "%-10s < %-10s = %d\n", $a, $b, cmp_versions($a, $b);
530: # }
531: #
532: # And the correct output is...
533: #
534: # 3.2 < 3.2 = 0
535: # 3.2a < 3.2a = 0
536: # 3.2 < 3.2a = -1
537: # 3.2 < 3.3 = -1
538: # 3.2 < 3.2.1 = -1
539: # 1.2.5i < 1.2.5.1 = -1
540: # 1.6.3p6 < 1.6.4 = -1
541: #
542: # the lexical sort does not give the correct result in the second to last case.
543:
544:
545: sub usage(){
546: die "usage: check-rpms [-v | --verbose] [-d directory | --dir directory]\n",
547: " [-ftp [server/directory]] [-noftp] [-lm | --list-missing]\n",
548: " [-lq | --list-questionable] [-r | --recheck ]\n",
549: " [-nk | --no-kernel] [--update] [-c configurationfile]\n";
550: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>