summaryrefslogtreecommitdiffstats
path: root/tools/draklive
diff options
context:
space:
mode:
Diffstat (limited to 'tools/draklive')
-rwxr-xr-xtools/draklive601
1 files changed, 601 insertions, 0 deletions
diff --git a/tools/draklive b/tools/draklive
new file mode 100755
index 000000000..127dd3157
--- /dev/null
+++ b/tools/draklive
@@ -0,0 +1,601 @@
+#!/usr/bin/perl
+
+# draklive $Id$
+
+# Copyright (C) 2005 Mandriva
+# Olivier Blin <oblin@mandriva.com>
+#
+# 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, 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 lib qw(/usr/lib/libDrakX);
+use MDK::Common;
+use list_modules;
+use modules;
+use run_program;
+use POSIX qw(strftime);
+use Cwd 'abs_path';
+use Getopt::Long;
+use Pod::Usage;
+
+my %predefined = (
+ mounts => {
+ simple_union => {
+ root => '/union',
+ overlay => 'unionfs',
+ dirs => [
+ {
+ mountpoint => '/memory',
+ type => 'tmpfs',
+ },
+ {
+ mountpoint => '/media',
+ },
+ ],
+ },
+ squash_union => {
+ root => '/union',
+ overlay => 'unionfs',
+ dirs => [
+ {
+ mountpoint => '/memory',
+ type => 'tmpfs',
+ },
+ {
+ mountpoint => '/system',
+ type => 'squashfs',
+ source => 'system.sqfs'
+ },
+ {
+ mountpoint => '/distrib',
+ type => 'squashfs',
+ source => 'distrib.sqfs',
+ build_from => '/',
+ },
+ ],
+ },
+ },
+ media => {
+ usb => {
+ storage => 'usb',
+ fs => 'vfat',
+ sleep => 15,
+ source => 'LABEL=MDVUSBROOT',
+ mountpoint => '/media',
+ },
+ cdrom => {
+ storage => 'cdrom',
+ fs => 'vfat',
+ source => 'LABEL=MDVUSBROOT',
+ mountpoint => '/media',
+ },
+ },
+);
+
+my %custom = (
+ media => {
+ nfs => sub {
+ my ($module, $client, $source) = @_;
+ {
+ modules => [ $module ],
+ fs => 'nfs',
+ pre => "ifconfig eth0 $client up",
+ source => $source,
+ mountpoint => '/media',
+ };
+ },
+ },
+);
+
+my %storage_modules = (
+ cdrom => "disk/cdrom|hardware_raid|sata|scsi bus/usb disk/raw",
+ usb => "bus/usb disk/raw|usb",
+);
+
+sub nls_modules {
+ my ($live) = @_;
+ if_($live->{media}{fs} eq 'vfat', 'nls_cp437'), #- default FAT codepage
+ map { "nls_$_" } (map { "iso8859-$_" } 1..7, 9, 13..15), 'utf8';
+}
+
+sub progress_start {
+ my ($total, $time, $o_exp_divide) = @_;
+ {
+ total => $total,
+ current => 0,
+ start_time => $time,
+ exp_divide => $o_exp_divide,
+ maxl => length($total) - $o_exp_divide,
+ };
+}
+
+sub progress_show_incr {
+ my ($progress, $incr, $time) = @_;
+ $progress->{current} += $incr;
+ my $elapsed_time = $time - $progress->{start_time};
+ my $eta = int($elapsed_time*$progress->{total}/$progress->{current});
+ printf("\r%3d%% (%$progress->{maxl}s/%-$progress->{maxl}s), %8s/%8s (ETA)",
+ int(100*$progress->{current}/$progress->{total}),
+ (map { substr($_, 0, length($_)-$progress->{exp_divide}) } $progress->{current}, $progress->{total}),
+ (map { strftime("%H:%M:%S", gmtime($_)) } $elapsed_time, $eta));
+}
+
+sub progress_end() { print "\n" }
+
+my %loop = (
+ squashfs => {
+ read_only => 1,
+ modules => [ qw(loop squashfs) ],
+ build => sub {
+ my ($root, $dest) = @_;
+ my $total = first(split /\s/, `du -sb $root`);
+ print "have to process " . int($total/1000000) . " MB\n";
+ my $progress = progress_start($total, time(), 6);
+ open(my $OUTPUT, '-|', 'mksquashfs', $root, $dest, '-info');
+ {
+ local $_; #- avoid outside $_ to be overwritten
+ while (<$OUTPUT>) {
+ if (/^mksquashfs: file .*, uncompressed size (\d+) bytes, (?:DUPLICATE)?$/) {
+ progress_show_incr($progress, $1, time());
+ }
+ }
+ }
+ progress_end();
+ },
+ mount => do { my $lo = 0; sub {
+ my ($live, $dir) = @_;
+ my @mnt = (
+ ($live->{media}{fs} eq 'nfs' ? '/bin/losetup -r' : 'losetup') .
+ " /dev/loop$lo $live->{media}{mountpoint}/$dir->{source}",
+ "mount -o ro -t squashfs /dev/loop$lo $dir->{mountpoint}");
+ $lo++;
+ @mnt;
+ } },
+ },
+ tmpfs => {
+ mount => sub {
+ my ($_live, $dir) = @_;
+ "mount -t tmpfs none $dir->{mountpoint}";
+ },
+ },
+);
+
+my %overlay = (
+ unionfs => {
+ modules => [ qw(unionfs) ],
+ mount => sub {
+ my ($live) = @_;
+ #- build dirs list: "dir1=ro:dir2:ro:dir3=rw"
+ my $dirs = join(':',
+ map { "$_->{mountpoint}=" .
+ ($_->{type} && !$loop{$_->{type}}{read_only} ? 'rw' : 'ro');
+ } @{$live->{mount}{dirs} || []});
+ "mount -o dirs=$dirs -t unionfs unionfs $live->{mount}{root}";
+ },
+ },
+);
+
+
+my %moddeps;
+sub load_moddeps {
+ my ($root, $kernel_path) = @_;
+ my $get_modname = sub { first($_[0] =~ m!^$kernel_path/kernel/(?:.*/|)(.*?)\.k?o!) };
+ %moddeps = (map {
+ my ($f, $deps) = split ':';
+ my $modname = $get_modname->($f);
+ $modname => { full => $f, deps => [ map { $get_modname->($_) } split ' ', $deps ] };
+ } cat_($root . $kernel_path . '/modules.dep'));
+}
+sub moddeps_closure {
+ my ($module) = @_;
+ my @deps = @{$moddeps{$module}{deps}};
+ (map { moddeps_closure($_) } @deps), @deps;
+}
+
+sub run_ {
+ print "running " . join(' ', @_) . "\n";
+ run_program::run(@_);
+}
+
+sub create_initrd {
+ my ($live) = @_;
+
+ rm_rf($live->{initrd_tree}) if -e $live->{initrd_tree};
+
+ mkdir_p($live->{initrd_tree} . $_) foreach
+ qw(/bin /dev /lib /proc),
+ $live->{media}{mountpoint},
+ (map { $_->{mountpoint} } @{$live->{mount}{dirs} || []}),
+ $live->{mount}{root};
+
+ # cp_f($live->{system}{root} . '/sbin/nash', $live->{initrd_tree} . '/bin/');
+ #- use nash from cooker for now, label support
+ cp_f('/sbin/nash', $live->{initrd_tree} . '/bin/');
+
+ if ($live->{debug} || $live->{media}{fs} eq 'nfs') {
+ cp_f('/lib/tls/libc.so.6', $live->{initrd_tree} . '/lib/');
+ cp_f('/lib/ld-linux.so.2', $live->{initrd_tree} . '/lib/');
+ }
+ if ($live->{media}{fs} eq 'nfs') {
+ cp_f('/sbin/ifconfig', $live->{initrd_tree} . '/bin/');
+ cp_f('/bin/mount', $live->{initrd_tree} . '/bin/');
+ cp_f('/sbin/losetup', $live->{initrd_tree} . '/bin/');
+ if ($live->{debug}) {
+ cp_f('/bin/ping', $live->{initrd_tree} . '/bin/');
+ cp_f('/lib/libresolv.so.2', $live->{initrd_tree} . '/lib/');
+ }
+ }
+ if ($live->{debug}) {
+ cp_f('/usr/bin/strace', $live->{initrd_tree} . '/bin/');
+ cp_f('/usr/bin/busybox', $live->{initrd_tree} . '/bin');
+ my @l = map { /functions:/ .. /^$/ ? do { s/\s//g; split /,/ } : () } `busybox`;
+ shift @l;
+ symlink('busybox', $live->{initrd_tree} . "/bin/$_") foreach @l;
+ }
+
+ require devices;
+ devices::make($live->{initrd_tree} . "/dev/$_") foreach
+ qw(console initrd null ram systty),
+ (map { "tty$_" } 0..5),
+ (map { "loop$_" } 0..7),
+ (map { $_, $_ . '1' } map { "sd$_" } ('a' .. 'h'));
+
+ load_moddeps($live->{system}{root}, "/lib/modules/" . $live->{system}{kernel});
+ my ($modules, $unknown) = partition { exists $moddeps{$_} }
+ uniq(map { modules::cond_mapping_24_26($_) } category2modules($storage_modules{$live->{media}{storage}})),
+ nls_modules($live),
+ $live->{media}{fs},
+ @{$live->{media}{modules} || []},
+ (map { @{$loop{$_}{modules} || []} } uniq(map { $_->{type} } grep { $_->{type} } @{$live->{mount}{dirs} || []})),
+ ($live->{mount}{overlay} ? @{$overlay{$live->{mount}{overlay}}{modules} || []} : ());
+ @$unknown and die "unknown modules:" . join("\n", '', @$unknown);
+
+ my @module_deps = uniq(map { moddeps_closure($_) } @$modules);
+ run_('gzip', '>', $live->{initrd_tree} . "/lib/$_.ko", '-dc', $live->{system}{root} . $moddeps{$_}{full})
+ foreach @module_deps, @$modules;
+
+ create_initrd_linuxrc($live, @module_deps, @$modules);
+ compress_initrd_tree($live);
+ add_splash($live);
+ $live->{copy_initrd} and cp_f($live->{boot_dir} . '/initrd.gz', $live->{copy_initrd});
+}
+
+sub create_initrd_linuxrc {
+ my ($live, @modules) = @_;
+ my $target = $live->{mount}{root} || $live->{media}{mountpoint};
+ output_with_perm($live->{initrd_tree} . '/linuxrc', 0755,
+ join("\n",
+ "#!/bin/nash",
+ (map { "insmod /lib/$_.ko" } @modules),
+ if_($live->{media}{sleep}, "sleep $live->{media}{sleep}"),
+ #- required for labels
+ "mount -t proc none /proc",
+ if_($live->{debug}, "/bin/sh"),
+ if_($live->{media}{pre}, deref_array($live->{media}{pre})),
+ ($live->{media}{fs} eq 'nfs' ? '/bin/mount -n -o ro,nolock' : 'mount') .
+ " -t $live->{media}{fs} $live->{media}{source} $live->{media}{mountpoint}",
+ (map { $loop{$_->{type}}{mount}->($live, $_) } grep { $_->{type} } @{$live->{mount}{dirs} || []}),
+ ($live->{mount}{overlay} ? $overlay{$live->{mount}{overlay}}{mount}->($live) : ()),
+ "echo 0x0100 > /proc/sys/kernel/real-root-dev",
+ "umount /proc",
+ ($live->{mount}{overlay} ?
+ # don't move to /initrd but /live, or else the overlay will be unmounted
+ ("mkdir -p $target/live", "pivot_root $target $target/live") :
+ "pivot_root $target $target/initrd"),
+ if_($live->{post}, deref_array($live->{post})),
+ ""));
+}
+
+sub compress_initrd_tree {
+ my ($live) = @_;
+
+ my $size = run_program::get_stdout("du -ks $live->{initrd_tree} | awk '{print \$1}'") + 250;
+ my $inodes = run_program::get_stdout("find $live->{initrd_tree} | wc -l") + 1250;
+ $size = int($size + $inodes / 10) + 1; #- 10 inodes needs 1K
+ my $initrd = $live->{boot_dir} . "/initrd";
+
+ mkdir_p($live->{boot_dir});
+ run_('dd', 'if=/dev/zero', "of=$initrd", 'bs=1k', "count=$size");
+ run_('mke2fs', '-q', '-m', 0, '-F', '-N', $inodes, '-s', 1, $initrd);
+ mkdir_p($live->{mnt});
+ run_('mount', '-o', 'loop', '-t', 'ext2', $initrd, $live->{mnt});
+ cp_af(glob("$live->{initrd_tree}/*"), $live->{mnt});
+ rm_rf($live->{mnt} . "/lost+found");
+ run_('umount', $live->{mnt});
+ run_('gzip', '-f', '-9', $initrd);
+}
+
+sub add_splash {
+ my ($live) = @_;
+ if ($live->{system}{vga_mode} && $live->{system}{splash} ne 'no') {
+ require bootloader;
+ my $initrd = "$live->{boot_dir}/initrd.gz";
+ my $tmp_initrd = '/tmp/initrd.gz';
+ cp_f($initrd, $live->{system}{root} . $tmp_initrd);
+ {
+ local $::prefix = $live->{system}{root};
+ bootloader::add_boot_splash($tmp_initrd, $live->{system}{vga_mode});
+ }
+ cp_f($live->{system}{root} . $tmp_initrd, $initrd);
+ unlink($live->{system}{root} . $tmp_initrd);
+ }
+}
+
+sub build_syslinux_cfg {
+ my ($live) = @_;
+ #- fastboot is needed to avoid fsck
+ my $append = "fastboot splash=silent vga=$live->{system}{vga_mode}";
+ qq(default live
+prompt 1
+timeout 40
+display live.msg
+label live
+ kernel vmlinuz
+ append initrd=initrd.gz $append
+);
+}
+
+sub install_system {
+ my ($live) = @_;
+ run_('drakx-in-chroot',
+ $live->{system}{repository},
+ $live->{system}{root},
+ if_($live->{system}{auto_install}, '--auto_install', abs_path($live->{system}{auto_install})),
+ if_($live->{system}{patch}, '--defcfg', abs_path($live->{system}{patch})))
+ or die "unable to install system chroot";
+
+ run_('urpmi',
+ '--root',
+ $live->{system}{root},
+ map { abs_path($_) } @{$live->{system}{rpms}}) if @{$live->{system}{rpms}};
+
+ #- make sure harddrake is run
+ #- (do it in chroot, or else Storable from the build box may write an incompatible config file)
+ system("chroot $live->{system}{root} " .
+ "perl -MStorable -e \"Storable::store({ UNKNOWN => {} }, '/etc/sysconfig/harddrake2/previous_hw')\"");
+
+ #- interactive mode can lead to race in initscripts
+ #- (don't use addVarsInSh from MDK::Common, it breaks shell escapes)
+ substInFile { s/^PROMPT=.*/PROMPT=no/ } $live->{system}{root} . '/etc/sysconfig/init';
+
+ #- disable first boot wizard
+ output($live->{system}{root} . '/etc/sysconfig/firstboot', 'FIRSTBOOT=no');
+ #- enable drakx-finish-install
+ output($live->{system}{root} . '/etc/sysconfig/finish-install', 'FINISH_INSTALL=yes');
+
+ #- preselect guest user in kdm
+ my $kdm_cfg = '/usr/share/config/kdm/kdmrc';
+ update_gnomekderc($live->{system}{root} . $kdm_cfg,
+ 'X-:0-Greeter' => (PreselectUser => 'Default', DefaultUser => 'guest')) if -f $kdm_cfg;
+}
+
+sub create_loopback_files {
+ my ($live) = @_;
+ mkdir_p($live->{images_dir});
+ foreach (grep { $_->{build_from} } @{$live->{mount}{dirs} || []}) {
+ my $tree = $live->{system}{root} . $_->{build_from};
+ my $dest = $live->{images_dir} . '/' . $_->{source};
+ unlink($dest);
+ $loop{$_->{type}}{build}->($tree, $dest);
+ }
+}
+
+sub get_media_label {
+ my ($live) = @_;
+ first($live->{media}{source} =~ /^LABEL=(.*)$/);
+}
+
+sub get_media_device {
+ my ($live) = @_;
+ return $live->{media}{device} if $live->{media}{device};
+ my $label = get_media_label($live) or return $live->{media}{source};
+ my $device = chomp_(`readlink -f /dev/disk/by-label/$label`)
+ or die "unable to find device for /dev/disk/by-label/$label";
+ $device;
+}
+
+sub prepare_bootloader {
+ my ($live) = @_;
+ mkdir_p($live->{boot_dir});
+ cp_f($live->{system}{root} . '/boot/vmlinuz-' . $live->{system}{kernel}, $live->{boot_dir} . '/vmlinuz');
+ my $msg = $live->{system}{root} . '/boot/message-graphic';
+ cp_f($msg, $live->{boot_dir} . '/live.msg') if -f $msg;
+ if ($live->{media}{storage} eq 'usb') {
+ output($live->{boot_dir} . '/syslinux.cfg', build_syslinux_cfg($live));
+ } elsif ($live->{media}{storage} eq 'cdrom') {
+ cp_f('/usr/lib/syslinux/isolinux-graphic.bin', $live->{boot_dir} . '/isolinux.bin');
+ output($live->{boot_dir} . '/isolinux.cfg', build_syslinux_cfg($live));
+ } else {
+ warn "not implemented yet";
+ }
+}
+
+sub create_master {
+ warn "not implemented yet";
+}
+
+sub record_master {
+ my ($live, $o_refresh_boot_only) = @_;
+ if ($live->{media}{storage} eq 'usb') {
+ my $label = get_media_label($live);
+ if ($live->{media}{device} && $label) {
+ run_('mkdosfs', '-n', $label, $live->{media}{device})
+ or die "unable to format device $live->{media}{device}";
+ }
+ my $device = get_media_device($live);
+ mkdir_p($live->{mnt});
+ run_('mount', $device, $live->{mnt})
+ or die "unable to mount $device";
+ cp_f(glob($live->{boot_dir} . '/*'), $live->{mnt});
+ output($live->{mnt} . '/syslinux.cfg', build_syslinux_cfg($live));
+ unless ($o_refresh_boot_only) {
+ foreach (grep { $_->{build_from} } @{$live->{mount}{dirs} || []}) {
+ print "copying $_->{source}\n";
+ cp_f($live->{images_dir} . '/' . $_->{source}, $live->{mnt});
+ }
+ }
+ run_('umount', $live->{mnt});
+ run_("syslinux $device") or die "unable to run syslinux on $device";
+ } else {
+ warn "not implemented yet";
+ }
+}
+
+sub record_boot {
+ my ($live) = @_;
+ record_master($live, 1);
+}
+
+sub complete_config {
+ my ($live) = @_;
+
+ #- set unsupplied config dirs
+ $live->{workdir} ||= '/tmp/draklive';
+ $live->{boot_dir} ||= $live->{workdir} . "/boot";
+ $live->{initrd_tree} ||= $live->{workdir} . "/initrd";
+ $live->{images_dir} ||= $live->{workdir} . "/images";
+ $live->{mnt} ||= $live->{workdir} . "/mnt";
+
+ #- check for minimum requirements
+ ref $live->{media} or die "no media definition";
+ ref $live->{system} or die "no system definition";
+ $live->{system}{kernel} or die "no kernel has been configured";
+ mkdir_p($live->{workdir});
+}
+
+sub clean {
+ my ($live) = @_;
+ rm_rf($_) foreach grep { -e $_ } $live->{workdir}, $live->{system}{root};
+}
+
+my @actions = (
+ { name => 'clean', do => \&clean },
+ { name => 'install', do => \&install_system },
+ { name => 'initrd', do => \&create_initrd },
+ { name => 'boot', do => \&prepare_bootloader },
+ { name => 'loop', do => \&create_loopback_files },
+ { name => 'master', do => \&create_master },
+ { name => 'record', do => \&record_master },
+ { name => 'record_boot', do => \&record_boot },
+);
+my @all = qw(install initrd boot loop master);
+
+my %live;
+GetOptions(
+ "help" => sub { pod2usage('-verbose' => 1) },
+ "all" => sub { $_->{to_run} = 1 foreach grep { member($_->{name}, @all) } @actions },
+ (map { $_->{name} => \$_->{to_run} } @actions),
+ "device:s" => sub { $live{media}{device} = $_[1] },
+ "config:s" => sub {
+ my $path = $_[1];
+ #- don't use do(), since it can't see lexicals in the enclosing scope
+ my $cfg = eval(cat_($path)) or die "unable to load $path";
+ add2hash(\%live, $cfg);
+ print "loaded $path as config file\n";
+ },
+) or pod2usage();
+unless (keys(%live)) {
+ warn 'no live definition';
+ pod2usage();
+}
+complete_config(\%live);
+
+require standalone;
+every { !$_->{to_run} } @actions and die 'nothing to do';
+foreach (grep { $_->{to_run} } @actions) {
+ print qq(* entering step "$_->{name}"\n);
+ $_->{do}->(\%live);
+ print qq(* step "$_->{name}" done\n);
+}
+
+__END__
+
+=head1 NAME
+
+draklive - A live distribution mastering tool
+
+=head1 SYNOPSIS
+
+draklive [options]
+
+ Options:
+ --help long help message
+
+ --install install selected distribution in chroot
+ --initrd build initrd
+ --boot prepare bootloader files
+ --loop build compressed loopback files
+ --master build master image
+
+ --all run all steps, from installation to recording
+
+ --clean clean installation chroot and work directory
+ --record install live on selected media
+ --record_boot install bootloader only on selected media
+
+ --device <dev> use this device for live recording, formatting
+ it preliminary (not needed if the device
+ already has the required label)
+ --config <file> use this configuration file as live description
+
+Examples:
+
+ draklive --config config/live.cfg --clean
+
+ draklive --config config/live.cfg --all
+
+ draklive --config config/live.cfg --record --device /dev/sdb1
+
+=head1 OPTIONS
+
+=over 8
+
+=item B<--config>
+
+Makes draklive use the next argument as a configuration file.
+This file should contain an hash describing the live distribution,
+meaning the system (chroot and boot), media (usb, cdrom, nfs),
+and mount type (simple R/W union, union with squash files).
+
+Here's a configuration sample:
+
+ {
+ system => {
+ root => '/chroot/live-move',
+ repository => '/mnt/ken/2006.0/i586',
+ kernel => '2.6.12-12mdk-i586-up-1GB',
+ auto_install => 'config/auto_inst.cfg.pl',
+ patch => 'config/patch-2006-live.pl',
+ rpms => [
+ 'rpms/unionfs-kernel-2.6.12-12mdk-i586-up-1GB-1.1.1.1.20051124.1mdk-1mdk.i586.rpm'
+ ],
+ vga_mode => 788,
+ },
+ media => $predefined{media}{usb},
+ mount => $predefined{mounts}{squash_union}
+ };
+
+=back
+
+=head1 DESCRIPTION
+
+B<draklive> builds a live distribution according to a
+configuration file, creates a master image,
+and optionnally installs it on a device.
+
+=head1 AUTHOR
+
+Olivier Blin <oblin@mandriva.com>
+
+=cut