#!/usr/bin/perl -w # # kernellab - manage kernel configs for many machines easily # Copyright (C) 1999 Tommi Virtanen # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # use strict; use vars qw(@BASEPATH @SOURCES @ALANCOX @MODULES @IMAGES @CONFIG $TEMPDIR $VERBOSE $DO_CONFIG); use vars qw($BUILDDIR $_host_regexp %FLAGS $MAKE_HEADERS $SOURCE_TYPE $PATCH_TYPE $KERNEL_REVISION $WRITE_CONFIG $WRITE_IMAGE); use File::Find; use POSIX qw(strftime); my $_programname=$0; $_programname=~s{^.*/}{}; sub fail(@) { die "$_programname: @_\n" } sub info(@) {print "$_programname: info: @_\n" if $VERBOSE} sub debug(@) {print "$_programname: debug: @_\n" if $VERBOSE>1} do "$ENV{HOME}/.kernellab.conf" or do '/etc/kernellab.conf' or fail "config error; $@\n"; sub find_first_matching(&@) { my $match = shift; foreach (@_) { return $_ if $match->($_); } return undef; } $BUILDDIR = $TEMPDIR . '/kernellab.' . time() . '.' . $$; @BASEPATH = map { append_slash($_) } grep {-d $_} @BASEPATH; @SOURCES = map { append_slash($_) } grep {-d $_} map { prefix_relative($_, @BASEPATH) } @SOURCES; @ALANCOX = map { append_slash($_) } grep {-d $_} map { prefix_relative($_, @BASEPATH) } @ALANCOX; @MODULES = map { append_slash($_) } grep {-d $_} map { prefix_relative($_, @BASEPATH) } @MODULES; @IMAGES = map { append_slash($_) } grep {-d $_} map { prefix_relative($_, @BASEPATH) } @IMAGES; @CONFIG = map { append_slash($_) } grep {-d $_} map { prefix_relative($_, @BASEPATH) } @CONFIG; $WRITE_IMAGE = find_first_matching {-w $_} @IMAGES or fail "cannot find a writable place to store kernel images\n", "perhaps you should run 'mkdir -p ~/kernellab/images'"; $WRITE_CONFIG = find_first_matching {-w $_} @CONFIG or fail "cannot find a writable place to store configs\n", "perhaps you should run 'mkdir -p ~/kernellab/configs'"; @BASEPATH and @SOURCES and @ALANCOX and @MODULES and @IMAGES and @CONFIG or fail "some of the base directories did not exist. Check config"; $_host_regexp = '[a-z0-9.-]+'; sub usage() { print < [[-ac] [..]] where options are -O, --official use official linux-x.x.x sources (default) -D, --debian use Debian kernel-source-x.x.x sources -a, --ac use Alan Cox patches (default) -p, --pre use Linus Thorvalds pre-patches --ben use Benjamin Herrenschmidt ben patches --benh use Benjamin Herrenschmidt benh patches --paulus use Paul Mackerras patches -c, --configure always run menuconfig -H, --headers also create a kernel-headers -package -v, --verbose be more verbose -h, --help show this help EOF } sub prefix($@;) { my ($file)=shift; return map {$_.$file} @_; } sub prefix_relative($@;) { my ($file) = shift; return $file if $file=~m{^[/.]}; return prefix($file, @_); } sub append_slash($;) { for (@_) { return m{ /$ }x ? $_ : $_.'/' } } sub getdir($;) { -d $_[0] or return (); opendir(DIR, $_[0]) or fail "cannot open directory $_[0]; $!"; my @r = readdir(DIR); closedir DIR; return @r; } sub _find_filename(@) { # for filename_* functions foreach (@_) { return $_ if -e $_ } return undef; } sub filename_kernel($$;) { my ($basename, $file) = @_; _find_filename(prefix($basename . '-' . $file . '.tar.bz2', @SOURCES), prefix($basename . '-' . $file . '.tar.gz', @SOURCES)); } sub filename_patch($$$;) { my ($k, $patch, $patchver) = @_; _find_filename(prefix('patch-' . $k . '-' . $patch . $patchver . '.bz2', @ALANCOX), prefix('patch-' . $k . '-' . $patch . $patchver . '.gz', @ALANCOX)); } sub filename_module($;) { _find_filename(prefix($_[0] . '.tar.gz', @MODULES), prefix($_[0] . '.tar.bz2', @MODULES)); } sub max(@) { my $max; foreach (@_) { $max=$_ if not defined $max or $max<$_; } return $max; } sub source_basename($;) { my ($source) = @_; my ($basename); for ($source) { /^debian$/ and $basename='kernel-source', next; /^official$/ and $basename='linux', next; fail "unknown kernel source type \"$source\""; } return $basename; } sub latest_kernel($$;) { my ($basename, $patch) = @_; my @kernels = map {/^$basename-(\d+)\.(\d+)\.(\d+)\./; [$_,$1,$2,$3]} grep {/^$basename-\d+\.\d+\.\d+\.tar\.(?:gz|bz2)$/} map {getdir $_} @SOURCES; my $a = max map {$_->[1]} @kernels; defined $a or fail "cannot determine latest kernel version (1)"; @kernels = grep {$_->[1] == $a} @kernels; my $b = max map {$_->[2]} @kernels; defined $b or fail "cannot determine latest kernel version (2)"; @kernels = grep {$_->[2] == $b} @kernels; my $c = max map {$_->[3]} @kernels; defined $c or fail "cannot determine latest kernel version (3)"; my $kernel = "$a.$b.$c"; my @patches = map {/^patch-\d+\.\d+\.\d+.-$patch(\d+\w*)\./; [$_,$1]} grep {/^patch-$a\.$b\.$c-$patch\d+\w*\.(?:gz|bz2)$/} map {getdir $_} @ALANCOX; my $patchver = max map {$_->[1]} @patches; $kernel .= '-' . $patch . $patchver if defined $patchver; return $kernel; } sub closest_config($$$;$;) { my ($host, $kver, $patch, $patchver) = @_; debug "finding closest config for $host $kver" . (defined $patchver ? "-$patch$patchver" : ''); my @configs; foreach my $confdir (@CONFIG) { foreach my $conffile (getdir $confdir) { $conffile =~ /^config-($_host_regexp)-(\d+)\.(\d+)\.(\d+)(?:-$patch(\d+)\w*)?$/ or next; $1 eq $host or next; push @configs, [$confdir.$conffile, $1,$2,$3,$4,$5]; } } @configs = sort { #descending $b->[2] <=> $a->[2] || $b->[3] <=> $a->[3] || $b->[4] <=> $a->[4] || $b->[5] <=> $a->[5] # rely on (undef<=>0)==(0<=>undef)==0 } @configs; return $configs[0]->[0]; } sub next_revision($$$$;) { my ($host, $kver, $date, $patch) = @_; my @revs = sort {$b<=>$a} map { $_->[3] } grep { $_->[1] eq $host and $_->[2] eq $date } map { /^kernel-image- (\d+\.\d+\.\d+(?:-$patch\d+\w*)?)_ # kernel version number ($_host_regexp)\. # hostname (\d\d\d\d\d\d\d\d)\. # yyyymmdd (\d+)_ # revision .*\.deb$ /x; [$1,$2,$3,$4] } grep {/^kernel-image-/} map {getdir "$_/$host"} @IMAGES; return "$host.$date.".($revs[0]+1) if @revs; return "$host.$date.1"; } sub extract($;) { my $cmd; for ($_[0]) { /\.bz2$/ and $cmd='/usr/bin/bzip2', next; /\.gz$/ and $cmd='/bin/gzip', next; fail "unknown package format, file $_"; } system('/bin/tar', '-xf', $_[0], '--use-compress-program', $cmd) == 0 or fail "unpacking $_[0] failed: $?"; } sub apply_patch($;) { debug "YOW!"; my ($file) = @_; debug "applying patch from file $file"; my $zcat; for ($file) { /\.bz2$/ and $zcat='/usr/bin/bzcat', next; /\.gz$/ and $zcat='/bin/zcat', next; fail "unknown patch compression, file $_"; } system("$zcat \"$file\" | patch -p1") == 0 or fail "applying patch $file failed: $?"; find(sub {/\.rej$/ and fail "patch $file failed for file $File::Find::name"}, '.'); } %FLAGS = ( v => 'verbose', verbose => sub {$VERBOSE++}, h => 'help', help => sub {usage(); exit(0)}, c => 'configure', config => 'configure', configure => sub {$DO_CONFIG++}, H => 'headers', headers => sub {$MAKE_HEADERS++}, O => 'official', official => sub {$SOURCE_TYPE = 'official'}, D => 'debian', debian => sub {$SOURCE_TYPE = 'debian'}, a => 'ac', ac => sub {$PATCH_TYPE = 'ac'}, p => 'pre', pre => sub {$PATCH_TYPE = 'pre'}, ben => sub {$PATCH_TYPE = 'ben'}, benh => sub {$PATCH_TYPE = 'benh'}, paulus => sub {$PATCH_TYPE = 'paulus'}, revision => sub {$KERNEL_REVISION = undef}, ); $SOURCE_TYPE = 'official'; $PATCH_TYPE = 'ac'; while (@ARGV and $ARGV[0] =~ /^-/) { local $_ = shift; s/^-//g; if (/^-/) { # long opt s/^-//g; exists $FLAGS{$_} or usage(), exit(1); $_=$FLAGS{$_} if not ref $FLAGS{$_}; &{$FLAGS{$_}}; } else { foreach (split //, $_) { exists $FLAGS{$_} or usage(), exit(1); $_=$FLAGS{$_} if not ref $FLAGS{$_}; &{$FLAGS{$_}}; } } } my ($host, $version, @modules) = @ARGV; defined $host and $host =~ /^$_host_regexp$/ or usage(), exit(1); my ($srcbasename) = source_basename($SOURCE_TYPE); $version = latest_kernel($srcbasename, $PATCH_TYPE) if not defined $version; my ($kernver, $kernver_first, $kernver_last, $patchver) = ($version =~ /^((\d+\.\d+\.)(\d+))(?:-$PATCH_TYPE(\d+\w*))?$/); defined $kernver or fail "invalid kernel version $version"; my ($kernfile); if ($PATCH_TYPE eq 'pre') { if ($kernver_last gt 0) { $kernfile=filename_kernel($srcbasename, $kernver_first . ($kernver_last - 1)); } else { fail "pre-patched x.x.0 sources not supported in kernellab (can't guess earlier version)"; } } else { $kernfile=filename_kernel($srcbasename, $kernver); } defined $kernfile or fail "kernel $kernver not found."; info "kernel version=$kernver, filename=$kernfile"; my $patchfile; if (defined $patchver) { $patchfile=filename_patch($kernver, $PATCH_TYPE, $patchver); defined $patchfile or fail "$PATCH_TYPE patch $kernver-$PATCH_TYPE$patchver not found."; info "$PATCH_TYPE patch $patchver, filename=$patchfile"; } if (@modules) { foreach (@modules) { defined filename_module($_) or fail "module $_ not found."; } info "modules=", join(', ', @modules); } info "making build dir"; mkdir $BUILDDIR, 0755 or fail "cannot mkdir build directory $BUILDDIR; $!"; chdir $BUILDDIR or fail "cannot chdir to build directory $BUILDDIR; $!"; info "extracting kernel sources"; extract($kernfile); if (-d "kernel-source-$kernver") { info "creating a symlink from '$kernfile' to 'linux'"; symlink ("kernel-source-$kernver",'linux') or fail "couldn't symlink 'kernel-source-$kernver' to 'linux'"; } -d 'linux' or fail "kernel source didn't unpack in 'linux'"; chdir 'linux' or fail "cannot chdir to kernel subdir; $!"; if (defined $patchfile) { info "applying $PATCH_TYPE patches"; apply_patch($patchfile); } # find closest config file, copy to .config my $config=closest_config($host, $kernver, $PATCH_TYPE, $patchver); if (defined $config) { #found one info "using old config $config"; system('cp', $config, '.config') == 0 or fail "copying $config to .config failed; $?"; system('make', 'oldconfig') == 0 or fail "make oldconfig failed; $?"; } else {$DO_CONFIG++} # default settings -> configure if ($DO_CONFIG) { system('make', 'menuconfig') == 0 or fail "make menuconfig failed; $?"; -e '.config' or fail "menuconfig didn't write a config file, exiting.."; } info "storing config"; system('cp', '.config', $WRITE_CONFIG . 'config-' . $host . '-' . $version) == 0 or fail "copying .config to $WRITE_CONFIG failed; $?"; info "building kernel..."; my $revision; if (defined $KERNEL_REVISION) { $revision = $KERNEL_REVISION ? "--revision $KERNEL_REVISION" : ""; } else { $revision = '--revision 1:'.next_revision($host, $kernver, strftime('%Y%m%d',localtime()), $PATCH_TYPE); } system('fakeroot', '/usr/bin/make-kpkg', "$revision", 'kernel_image') == 0 or fail "make-kpkg kernel_image failed; $?"; if ($MAKE_HEADERS) { info "building headers..."; system('fakeroot', '/usr/bin/make-kpkg', 'kernel_headers') == 0 or fail "make-kpkg kernel_headers failed; $?"; } chdir $BUILDDIR or fail "cannot chdir to build directory $BUILDDIR; $!"; info "extracting modules"; #modules have to unpack into modules/ foreach(@modules) { extract filename_module $_; } if (@modules) { # this check is too many false positives -d 'modules' or fail "modules didn't unpack in 'modules'"; chdir 'linux' or fail "cannot chdir to kernel subdir; $!"; info "building modules..."; $ENV{MODULE_LOC}=$BUILDDIR . '/modules'; # $ENV{SRCTOP}=$BUILDDIR . '/linux'; # $ENV{PWD}=$BUILDDIR . '/linux'; system('fakeroot', '/usr/bin/make-kpkg', 'modules_image') == 0 or fail "make-kpkg modules_image failed; $?"; } chdir $BUILDDIR or fail "cannot chdir to build directory $BUILDDIR; $!"; info "cleaning"; system('rm', '-rf', 'linux', 'modules', 'kernel-source-*'); info "moving images to", $WRITE_IMAGE.$host; -d "$WRITE_IMAGE/$host" or mkdir "$WRITE_IMAGE/$host", 0755 or fail "cannot make directory $WRITE_IMAGE/$host; $!"; my $image_exists_error = defined($KERNEL_REVISION) ? "don't build with same revision twice!" : "this can't happen!"; foreach (grep {!/^\./} getdir '.') { fail "$_ exists in $WRITE_IMAGE, $image_exists_error" if -e "$WRITE_IMAGE/$host/$_"; system('mv', '-i', $_, "$WRITE_IMAGE/$host/$_") == 0 or fail "moving images failed on file $_: $!"; } info "final cleaning"; chdir '/' or fail "cannot chdir out from build dir; $!"; rmdir $BUILDDIR or fail "cannot remove build dir $BUILDDIR; $!"; print "$_programname: Done.\n"; exit(0); __END__ =head1 NAME kernellab - manage kernel configs for many machines easily =head1 SYNOPSIS kernellab [options] [[-ac] [..]] =head1 DESCRIPTION Kernellab helps you manage kernel configs for many heterogenous machines. The configs are just stored in their normal format in /var/state/kernellab/configs/config--[-ac]. This and placing the kernel sources in a format accessible to kernellab allows you to easily build a new kernel for your computers. Let's take an example: say you have 20 miscellanous machines working as routers all over your network, with different ethernet cards and other kernel options. Say someone discovers a denial of service -attack in the linux TCP/IP stack. So you wait two hours till Alan Cox puts out a new -ac42 patch, download this patch and put in to /var/state/kernellab/alancox/patch-n.n.n-ac42.bz2. Now, all you need to do to recompile the new, fixed, kernel for all your routers, is for a in router1 router2 router3 ...; do kernellab "$a"; done =head1 OPTIONS -O use official linux-x.x.x sources (default) -D use Debian kernel-source-x.x.x sources -a use Alan Cox patches (default) -p use Linus Thorvalds pre-patches --ben use Benjamin Herrenschmidt ben patches --benh use Benjamin Herrenschmidt benh patches --paulus use Paul Mackerras patches -c always run make menuconfig -H also create a kernel-headers -package -v increase verbosity (can specify many times) -h show usage =head1 BUGS This manpage. =head1 AUTHOR Tommi Virtanen =cut