[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Re: libata transition script



On Sat, 2009-12-12 at 04:58 +0000, Ben Hutchings wrote:
> Here's what I've got so far.  There's a lot still to do, but it's
> reached the point of being able to convert the fstab on my laptop.
> 
> TODO:
> - Only change device id in boot loader configurations that use an
> initramfs
Not done, but I do now limit to kernel parameters that are applied
globally or to the sym-links /vmlinu[xz] and /vmlinu[xz].old.

> - Warn about configuration files that may need to be updated manually
Done.

> - Run post-update commands for installed packages
Done.

> - Review error handling
> - Allow user to adjust the plan?
> - Other debconf refinements
> - Is there anything we can do about CD-ROMs?  Can we add a 'scsi' path
> rule in /etc/udev/rules.d/70-persistent-cd.rules after each 'ide' path
> rule?
The user will now be notified about references to CD-ROM device names,
but no changes will be made automatically.  This should be fixed.

> - Similarly for tape drives?  (low priority as they're far less common)
> - Get maintainers of all affected packages to review the relevant code

I want to move on to this as soon as possible.

> I was thinking of creating a new package linux-image-2.6-common that all
> image packages will Depend on, and putting this in that package.  Any
> objections to that?

Please speak up!

The current version of the script is below.

Ben.

#!/usr/bin/perl

# Copyright 2009 Ben Hutchings
#
# 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

use strict;
use warnings;
use Debconf::Client::ConfModule ':all';
use FileHandle;
use POSIX ();

### utility

sub id_to_path {
    my ($id) = @_;
    $id =~ m|^/|
	or $id =~ s{^(LABEL|UUID)=}{'/dev/disk/by-' . lc($1) . '/'}x
	or die "Could not map id $id to path";
    return $id;
}

### /etc/fstab

sub fstab_next {
    # Based on my_getmntent() in mount_mntent.c

    my ($file) = @_;
    my $text = <$file>;
    unless (defined($text)) {
	return ();
    }

    my $line = $text;
    $line =~ s/\r?\n$//;
    $line =~ s/^[ \t]*//;
    if ($line =~ /^(#|$)/) {
	return ($text);
    } else {
	return ($text,
		map({ s/\\([0-7][0-7][0-7])/chr(oct($1) & 0xff)/eg; $_; }
		    split(/[ \t]+/, $line)));
    }
}

sub fstab_list {
    my ($file) = @_;
    my @bdevs;
    while (1) {
	my ($text, $bdev) = fstab_next($file);
	last unless defined($text);
	if (defined($bdev)) {
	    push @bdevs, $bdev;
	}
    }
    return @bdevs;
}

sub fstab_update {
    my ($old, $new, $map) = @_;
    while (1) {
	my ($text, $bdev) = fstab_next($old);
	last unless defined($text);
	if (defined($bdev) && defined(my $id = $map->{$bdev})) {
	    $text =~ s/^(\s*\S+)(.*)/# $1$2\n$id$2/;
	}
	$new->print("$text");
    }
}

### Kernel parameters

sub kernel_list {
    my ($cmd_line) = @_;
    return ($cmd_line =~ /\broot=(\S+)/) ? ($1) : ();
}

sub kernel_update {
    my ($cmd_line, $map) = @_;
    if ($cmd_line =~ /\broot=(\S+)/ && defined(my $id = $map->{$1})) {
	$cmd_line =~ s/\broot=(\S+)/root=$id/;
	return $cmd_line;
    } else {
	return undef;
    }
}

### shell script variable assignment

# Maintains enough context to find statement boundaries, and can parse
# variable definitions that do not include substitutions.  I think.

sub shellvars_next {
    my ($file) = @_;
    my $text = '';
    my @context = ('');
    my $first = 1;
    my $in_value = 0;
    my ($name, $value);
    my $unhandled = 0;

  LINE:
    while (<$file>) {
	$text .= $_;

	# variable assignment
	if ($first && m/^\s*([A-Za-z_][A-Za-z0-9_]*)=/g) {
	    $name = $1;
	    $value = '';
	    $in_value = 1;
	}

	while (/\G(.*?)([#`'"(){}\s]|\\.|\$[({]?)/gs) {
	    my $end_pos = pos;
	    my $special = $2;

	    if ($in_value) {
		# add non-special characters to the value verbatim
		$value .= $1;
	    }

	    if ($context[$#context] eq '') {
		# space outside quotes or brackets ends the value
		if ($special =~ /^\s/) {
		    $in_value = 0;
		    if ($special eq "\n") {
			last LINE;
		    }
		}
		# something else after the value means this is a command
		# with an environment override, not a variable definition
		elsif (defined($name) && !$in_value) {
		    $unhandled = 1;
		}
	    }

	    # in single-quoted string
	    if ($context[$#context] eq "'") {
		# only the terminating single-quote is special
		if ($special eq "'") {
		    pop @context;
		} else {
		    $value .= $special;
		}
	    }
	    # backslash escape
	    elsif ($special =~ /^\\/) {
		if ($in_value && $special ne "\\\n") {
		    $value .= substr($special, 1, 1);
		}
	    }
	    # in backtick substitution
	    elsif ($context[$#context] eq '`') {
		# backtick does not participate in nesting, so only the
		# terminating backtick should be considered special
		if ($special eq '`') {
		    pop @context;
		}
	    }
	    # comment
	    elsif ($context[$#context] !~ /^['"]/ && $special eq '#') {
		# ignore rest of the physical line, except the new-line
		pos = $end_pos;
		/\G.*/g;
		next;
	    }
	    # start of backtick substitution
	    elsif ($special eq '`') {
		push @context, '`';
		$unhandled = 1;
	    }
	    # start of single/double-quoted string
	    elsif ($special =~ /^['"]/ && $context[$#context] !~ /^['"]/) {
		push @context, $special;
	    }
	    # end of double-quoted string
	    elsif ($special eq '"' && $context[$#context] eq '"') {
		pop @context;
	    }
	    # open bracket
	    elsif ($special =~ /^\$?\(/) {
		push @context, ')';
		$unhandled = 1;
	    } elsif ($special =~ /^\$\{/) {
		push @context, '}';
		$unhandled = 1;
	    }
	    # close bracket
	    elsif ($special =~ /^[)}]/ && $special eq $context[$#context]) {
		pop @context;
	    }
	    # variable substitution
	    elsif ($special eq '$') {
		$unhandled = 1;
	    }
	    # not a special character in this context (or a syntax error)
	    else {
		if ($in_value) {
		    $value .= $special;
		}
	    }

	    pos = $end_pos;
	}

	$first = 0;
    }

    if ($text eq '') {
	return ();
    } elsif ($unhandled) {
	return ($text);
    } else {
	return ($text, $name, $value);
    }
}

sub shellvars_quote {
    my ($value) = @_;
    $value =~ s/'/'\''/g;
    return "'$value'";
}

### GRUB 1 (grub-legacy) config

sub grub1_parse {
    my ($file) = @_;
    my @results = ();
    my $text = '';
    my $in_auto = 0;
    my $in_opts = 0;

    while (<$file>) {
	if ($in_opts && /^\# (\w+)=(.*)/) {
	    push @results, [$text];
	    $text = '';
	    push @results, [$_, $1, $2];
	} else {
	    $text .= $_;
	    if ($_ eq "### BEGIN AUTOMAGIC KERNELS LIST\n") {
		$in_auto = 1;
	    } elsif ($_ eq "### END DEBIAN AUTOMAGIC KERNELS LIST\n") {
		$in_auto = 0;
	    } elsif ($_ eq "## ## Start Default Options ##\n") {
		$in_opts = $in_auto;
	    } elsif ($_ eq "## ## End Default Options ##\n") {
		$in_opts = 0;
	    }
	}
    }

    if ($text ne '') {
	push @results, [$text];
    }

    return @results;
}

sub grub1_list {
    my ($file) = @_;
    my %options;
    for (grub1_parse($file)) {
	my ($text, $name, $value) = @$_;
	next unless defined($name);
	$options{$name} = $value;
    }

    my @bdevs;
    if (exists($options{kopt_2_6})) {
	push @bdevs, kernel_list($options{kopt_2_6});
    } elsif (exists($options{kopt})) {
	push @bdevs, kernel_list($options{kopt});
    }
    if (exists($options{xenkopt})) {
	push @bdevs, kernel_list($options{xenkopt});
    }
    return @bdevs;
}

sub grub1_update {
    my ($old, $new, $map) = @_;

    my %options;
    for (grub1_parse($old)) {
	my ($text, $name, $value) = @$_;
	next unless defined($name);
	$options{$name} = $value;
    }

    $old->seek(0, 0);
    for (grub1_parse($old)) {
	my ($text, $name, $value) = @$_;
	next unless defined($name);
	if ($name eq 'kopt_2_6' ||
	    ($name eq 'kopt' && !exists($options{kopt_2_6})) ||
	    $name eq 'xenkopt') {
	    if (defined(my $new_value = kernel_update($value))) {
		$text = "## $name=$value\n# $name=$new_value\n";
	    }
	}
	$new->print($text);
    }
}

sub grub1_post {
    system('update-grub');
}

### GRUB 2 config

sub grub2_list {
    my ($file) = @_;
    my @bdevs;

    while (1) {
	my ($text, $name, $value) = shellvars_next($file);
	last unless defined($text);
	if (defined($name) && $name =~ /^GRUB_CMDLINE_LINUX(?:_DEFAULT)?$/) {
	    push @bdevs, kernel_list($value);
	}
    }

    return @bdevs;
}

sub grub2_update {
    my ($old, $new, $map) = @_;
    my @bdevs;

    while (1) {
	my ($text, $name, $value) = shellvars_next($old);
	last unless defined($text);
	if (defined($name) && $name =~ /^GRUB_CMDLINE_LINUX(?:_DEFAULT)?$/ &&
	    defined(my $new_value = kernel_update($value, $map))) {
	    $text =~ s/^/# /gm;
	    $text .= sprintf("%s=%s\n", $name, shellvars_quote($new_value));
	}
	$new->print($text);
    }
}

sub grub2_post {
    system('grub-mkconfig', '-o', '/boot/grub/grub.cfg');
}

### LILO

sub lilo_tokenize {
    # Based on cfg_get_token() and next() in cfg.c.
    # Line boundaries are *not* significant (except as white space) so
    # we tokenize the whole file at once.

    my ($file) = @_;
    my @tokens = ();
    my $text = '';
    my $token;
    my $in_quote = 0;

    while (<$file>) {
	# If this is the continuation of a multi-line quote, skip
	# leading space and push back the necessary context.
	if ($in_quote) {
	    s/^[ \t]*/"/;
	    $text .= $&;
	}

	pos = 0;
	while (/\G \s* (?:\#.*)?
                (?: (=) |
                    " ((?:[^"] | \\[\\"n])*) (" | \\\r?\n) |
                    ((?:[^\s\#="\\] | \\[^\r\n])+) (\\\r?\n)?)?
               /gsx) {
	    my $cont;
	    my $new_text = $&;

	    if (defined($1)) {
		# equals sign
		$text = $new_text;
		$token = $1;
		$cont = 0;
	    } elsif (defined($2)) {
		# quoted text
		if (!$in_quote) {
		    $text = $new_text;
		    $token = $2;
		} else {
		    $text .= substr($new_text, 1); # remove the quote again; ick
		    $token .= ' ' . $2;
		}
		$cont = $3 ne '"';
	    } elsif (defined($4)) {
		# unquoted word
		if (!defined($token)) {
		    $token = '';
		}
		$text .= $new_text;
		$token .= $4;
		$cont = defined($5);
	    } else {
		$text .= $new_text;
		$cont = $new_text eq '';
	    }

	    if (!$cont) {
		if ($text =~ /(?:^|[^\\])\$/) {
		    # unhandled expansion
		    $token = undef;
		} elsif (defined($token)) {
		    if ($in_quote) {
			$token =~ s/\\(.)/$1 eq 'n' ? "\n" : $1/eg;
		    } else {
			$token =~ s/\\(.)/$1/g;
		    }
		}
		push @tokens, [$text, $token];
		$text = '';
		$token = undef;
		$in_quote = 0;
	    }
	}
    }

    return @tokens;
}

sub lilo_list {
    my ($file) = @_;
    my @bdevs = ();
    my @tokens = lilo_tokenize($file);
    my $i = 0;
    my $in_generic = 1; # global or image=/vmlinuz or image=/vmlinuz.old

    while ($i <= $#tokens) {
	# Configuration items are either <name> "=" <value> or <name> alone.
	if ($#tokens - $i >= 2 &&
	    defined($tokens[$i + 1][1]) && $tokens[$i + 1][1] eq '=') {
	    my ($name, $value) = ($tokens[$i][1], $tokens[$i + 2][1]);
	    if (defined($name) && defined($value)) {
		if ($name eq 'image') {
		    $in_generic = ($value =~ m|^/vmlinuz(?:\.old)?$|);
		    next;
		}
		if (!$in_generic) {
		    next;
		}
		if ($name =~ /^(?:boot|root)$/) {
		    push @bdevs, $value;
		} elsif ($name =~ /^(?:addappend|append|literal)$/) {
		    push @bdevs, kernel_list($value);
		}
	    }
	    $i += 3;
	} else {
	    $i += 1;
	}
    }

    return @bdevs;
}

sub lilo_update {
    my ($old, $new, $map) = @_;
    my @tokens = lilo_tokenize($old);
    my $i = 0;
    my $in_generic = 1; # global or image=/vmlinuz or image=/vmlinuz.old

    while ($i <= $#tokens) {
	my $text = $tokens[$i][0];

	if ($#tokens - $i >= 2 &&
	    defined($tokens[$i + 1][1]) && $tokens[$i + 1][1] eq '=') {
	    my ($name, $value) = ($tokens[$i][1], $tokens[$i + 2][1]);
	    my $new_value;
	    if (defined($name) && defined($value)) {
		if ($name eq 'image') {
		    $in_generic = ($value =~ m|^/vmlinuz(?:\.old)?$|);
		    next;
		}
		if (!$in_generic) {
		    next;
		}
		if ($name eq 'boot') {
		    # 'boot' is used directly by the lilo command, which
		    # doesn't use libblkid
		    $new_value = $map->{$value} && id_to_path($map->{$value});
		} elsif ($name eq 'root') {
		    # 'root' adds a root parameter to the kernel command
		    # line
		    $new_value = $map->{$value};
		} elsif ($name =~ /^(?:addappend|append|literal)$/) {
		    # These are all destined for the kernel command line
		    # in some way
		    $new_value = kernel_update($value, $map);
		}
	    }
	    if (defined($new_value)) {
		$text = "\n# $name = $value\n$name = $new_value\n";
	    } else {
		$text .= $tokens[$i + 1][0] . $tokens[$i + 2][0];
	    }
	    $i += 3;
	} else {
	    $i += 1;
	}

	$new->print($text);
    }
}

sub lilo_post {
    system('lilo');
}

### ELILO

sub elilo_post {
    system('elilo');
}

### PALO

sub palo_next {
    my ($file, $expect_opt) = @_;
    my $text = <$file>;

    if (!defined($text) || $text eq '') {
	return ();
    }

    my $arg = $text;
    $arg =~ s/^\s*(?:#.*)?//s;
    $arg =~ s/\s*$//;

    # I would like to use Getopt::Long but it would make it
    # impossible to determine which source text to replace.
    if ($expect_opt && $arg =~ /^-(?!-)[?v]*(.)(.+)?$/) {
	return ($text, "-$1", $2,    defined($2));
    } elsif ($expect_opt && $arg =~ /^(--[^=]+)(?:=(.*))?$/) {
	return ($text, $1,    $2,    defined($2));
    } elsif ($arg ne '') {
	return ($text, undef, $arg,  1);
    } else {
	return ($text, undef, undef, $expect_opt);
    }
}

sub palo_list {
    my ($file) = @_;
    my $optopt;
    my @bdevs;

    while (1) {
	my ($text, $optarg, $complete);
	if (defined($optopt)) {
	    ($text, undef,   $optarg, $complete) = palo_next($file, 0);
	} else {
	    ($text, $optopt, $optarg, $complete) = palo_next($file, 1);
	}
	last unless defined($text);

	if ($complete && defined($optopt)) {
	    if ($optopt eq '-c' || $optopt eq '--commandline') {
		# If PALO is not configured to use the generic sym-link,
		# ignore it
		if ($optarg !~ m|^\d+/vmlinux\b|) {
		    return ();
		}
		push @bdevs, kernel_list($optarg);
	    } elsif ($optopt eq '-I' || $optopt eq '--init-partitioned') {
		push @bdevs, $optarg;
	    }
	    $optopt = undef;
	}

	if (!defined($optopt) && defined($optarg) && $optarg eq '--') {
	    last;
	}
    }

    return @bdevs;
}

sub palo_update {
    my ($old, $new, $map) = @_;
    my $optopt;
    my $allow_opts = 1;

    while (1) {
	my ($text, $optarg, $complete);
	if (defined($optopt)) {
	    ($text, undef,   $optarg, $complete) = palo_next($old, 0);
	} else {
	    ($text, $optopt, $optarg, $complete) = palo_next($old, $allow_opts);
	}
	last unless defined($text);

	if (defined($optopt)) {
	    if ($optopt eq '-c' || $optopt eq '--commandline') {
		$text = "# $text";
		if ($complete) {
		    my $new_cmdline = kernel_update($optarg, $map);
		    if (!defined($new_cmdline)) {
			$new_cmdline = $optarg;
		    }
		    $text .= "--commandline=$new_cmdline\n";
		}
	    }
	    $optopt = undef;
	}

	$new->print($text);

	if (!defined($optopt) && defined($optarg) && $optarg eq '--') {
	    $allow_opts = 0;
	}
    }
}

sub palo_post {
    system('palo');
}

### delo

sub delo_next {
    # Based on getconfig() in config.c

    my ($file) = @_;
    my $text = <$file>;

    if (!defined($text) || $text eq '') {
	return ();
    }

    local $_ = $text;
    s/[ \t]*(?:#.*)?\n//;
    s/^[ \t]*//;

    if (/^([a-z]+)=(.*)$/) {
	return ($text, $1, $2);
    } else {
	return ($text);
    }
}

sub delo_sections {
    my ($file) = @_;
    my @sections;
    my $section = {};

    while (1) {
	my ($text, $name, $value) = delo_next($file);

	# If this is EOF or a new section, finish the current section
	if (!defined($text) || (defined($name) && $name eq 'label')) {
	    $section->{is_generic} =
		(exists($section->{image}) &&
		 exists($section->{append}) &&
		 $section->{image} =~ m|^/vmlinux(?:\.old)?$|);
	    push @sections, $section;
	    $section = {};
	}

	last unless defined($text);

	if (defined($name)) {
	    if ($name eq 'append') {
		$value =~ s/^"([^"]*).*/$1/;
	    }
	    $section->{$name} = $value;
	}
    }

    return @sections;
}

sub delo_list {
    my ($file) = @_;
    my ($globals, @entries) = delo_sections($file);
    my @bdevs;

    if (exists($globals->{boot})) {
	push @bdevs, $globals->{boot};
    }

    for my $entry (@entries) {
	if ($entry->{is_generic}) {
	    push @bdevs, kernel_list($entry->{append});
	}
    }

    return @bdevs;
}

sub delo_update {
    my ($old, $new, $map) = @_;
    my ($globals, @entries) = delo_sections($old);
    my $i = -1;

    $old->seek(0, 0);

    while (1) {
	my ($text, $name, $value) = delo_next($old);
	last unless defined($text);

	if (defined($name)) {
	    if ($name eq 'label') {
		++$i; # next entry
	    } elsif ($name eq 'boot' && $i < 0) {
		my $new_value = $map->{$value} && id_to_path($map->{$value});
		if (defined($new_value)) {
		    $text = "# $text" . "boot=$new_value\n";
		}
	    } elsif ($name eq 'append' &&
		     $i >= 0 && $entries[$i]->{is_generic}) {
		my $new_cmdline = kernel_update($value, $map);
		if (defined($new_cmdline)) {
		    $text = "# $text" . "append=\"$new_cmdline\"\n";
		}
	    }
	}

	$new->print($text);
    }
}

### extlinux

sub extlinux_path {
    for ('/boot/extlinux', '/boot/boot/exlinux', '/extlinux') {
	if (-e) {
	    return "$_/options.cfg";
	}
    }
    return undef;
}

sub extlinux_list {
    my ($file) = @_;
    while (<$file>) {
	if (/^## ROOT=(.*)/) {
	    return kernel_list($1);
	}
    }
    return ();
}

sub extlinux_update {
    my ($old, $new, $map) = @_;
    while (<$old>) {
	my $text = $_;
	if (/^## ROOT=(.*)/) {
	    my $new_params = kernel_update($1, $map);
	    if (defined($new_params)) {
		$text = "## $text" . "## ROOT=$new_params\n";
	    }
	}
	$new->print($text);
    }
}

sub extlinux_post {
    system('update-extlinux');
}

### aboot

sub aboot_next {
    my ($file) = @_;
    my $text = <$file>;

    if (!defined($text) || $text eq '') {
	return ();
    }

    if ($text =~ /^([0-9]):([^ ]*) (.*)/) {
	return ($text, $1, $2, $3);
    } else {
	return ($text);
    }
}

sub aboot_list {
    my ($file) = @_;
    my @bdevs;
    while (1) {
	my ($text, $preset, $kernel, $params) = aboot_next($file);
	last unless defined($text);
	if (defined($params) && $kernel =~ m|^\d+/vmlinux(?:\.old)?$|) {
	    push @bdevs, kernel_list($params);
	}
    }
    return @bdevs;
}

sub aboot_update {
    my ($old, $new, $map) = @_;
    while (1) {
	my ($text, $preset, $kernel, $params) = aboot_next($old);
	last unless defined($text);
	if (defined($params) && $kernel =~ m|^\d+/vmlinux(?:\.old)?$|) {
	    my $new_params = kernel_update($params, $map);
	    if (defined($new_params)) {
		$text = "# $text" . "$preset:$kernel $new_params\n";
	    }
	}
	$new->print($text);
    }
}

### Filesystem relabelling

sub ext2_label {
    my ($bdev, $label) = @_;
    system('e2label', $bdev, $label) == 0 or die "e2label failed: $?";
}

sub jfs_label {
    my ($bdev, $label) = @_;
    system('jfs_tune', '-L', $label, $bdev) == 0 or die "jfs_tune failed: $?";
}

sub fat_label {
    my ($bdev, $label) = @_;
    system('dosfslabel', $bdev, $label) == 0 or die "dosfslabel failed: $?";;
}

sub ntfs_label {
    my ($bdev, $label) = @_;
    system('ntfslabel', $bdev, $label) == 0 or die "ntfslabel failed: $?";
}

sub reiserfs_label {
    my ($bdev, $label) = @_;
    system('reiserfstune', '--label', $label, $bdev)
	or die "reiserfstune failed: $?";
}

# There is no command to relabel swap, and we mustn't run mkswap if
# the partition is already in use.  Thankfully the header format is
# pretty simple; it starts with this structure:
# struct swap_header_v1_2 {
# 	char	      bootbits[1024];    /* Space for disklabel etc. */
# 	unsigned int  version;
# 	unsigned int  last_page;
# 	unsigned int  nr_badpages;
# 	unsigned char uuid[16];
# 	char	      volume_name[16];
# 	unsigned int  padding[117];
# 	unsigned int  badpages[1];
# };
# and has the signature 'SWAPSPACE2' at the end of the first page.
use constant { SWAP_SIGNATURE => 'SWAPSPACE2',
	       SWAP_LABEL_OFFSET => 1052, SWAP_LABEL_LEN => 16 };
sub swap_label {
    my ($bdev, $label) = @_;
    my $pagesize = POSIX::sysconf(POSIX::_SC_PAGESIZE) or die "$!";
    my ($length, $signature);

    my $fd = POSIX::open($bdev, POSIX::O_RDWR);
    defined($fd) or die "$!";

    # Check the signature
    POSIX::lseek($fd, $pagesize - length(SWAP_SIGNATURE), POSIX::SEEK_SET);
    $length = POSIX::read($fd, $signature, length(SWAP_SIGNATURE));
    if (!defined($length) || $signature ne SWAP_SIGNATURE) {
	POSIX::close($fd);
	die "swap signature not found on $bdev";
    }

    # Set the label
    $label = pack('Z' . SWAP_LABEL_LEN, $label);
    POSIX::lseek($fd, SWAP_LABEL_OFFSET, POSIX::SEEK_SET);
    $length = POSIX::write($fd, $label, SWAP_LABEL_LEN);
    if (!defined($length) || $length != SWAP_LABEL_LEN) {
	my $error = "$!";
	POSIX::close($fd);
	die $error;
    }

    POSIX::close($fd);
}

sub ufs_label {
    my ($bdev, $label) = @_;
    system('tunefs.ufs', '-L', $label, $bdev) or die "tunefs.ufs failed: $?";
}

sub xfs_label {
    my ($bdev, $label) = @_;
    system('xfs_admin', '-L', $label, $bdev) or die "xfs_admin failed: $?";
}

my %label_types = (ext2     => { len => 16,  relabel => \&ext2_label },
		   ext3     => { len => 16,  relabel => \&ext2_label },
		   ext4     => { len => 16,  relabel => \&ext2_label },
		   jfs      => { len => 16,  relabel => \&jfs_label },
		   msdos    => { len => 11,  relabel => \&fat_label },
		   ntfs     => { len => 128, relabel => \&ntfs_label },
		   reiserfs => { len => 16,  relabel => \&reiserfs_label },
		   swap     => { len => SWAP_LABEL_LEN,
				 relabel => \&swap_label },
		   ufs      => { len => 32,  relabel => \&ufs_label },
		   vfat     => { len => 11,  relabel => \&fat_label },
		   xfs      => { len => 12,  relabel => \&xfs_label });

### general

my @config_files = ({packages => 'mount',
		     path => '/etc/fstab',
		     list => \&fstab_list,
		     update => \&fstab_update},
		    {packages => 'grub grub-legacy',
		     path => '/boot/grub/menu.lst',
		     list => \&grub1_list,
		     update => \&grub1_update,
		     post_update => \&grub1_post},
		    {packages => 'grub-common',
		     path => '/etc/default/grub',
		     list => \&grub2_list,
		     update => \&grub2_update,
		     post_update => \&grub2_post},
		    {packages => 'lilo',
		     path => '/etc/lilo.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     post_update => \&lilo_post},
		    {packages => 'silo',
		     path => '/etc/silo.conf',
		     list => \&lilo_list,
		     update => \&lilo_update},
		    {packages => 'quik',
		     path => '/etc/quik.conf',
		     list => \&lilo_list,
		     update => \&lilo_update},
		    {packages => 'yaboot',
		     path => '/etc/yaboot.conf',
		     list => \&lilo_list,
		     update => \&lilo_update},
		    {packages => 'elilo',
		     path => '/etc/elilo.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     post_update => \&elilo_post},
		    {packages => 'palo',
		     path => '/etc/palo.conf',
		     list => \&palo_list,
		     update => \&palo_update,
		     post_update => \&palo_post},
		    {packages => 'delo',
		     path => '/etc/delo.conf',
		     list => \&delo_list,
		     update => \&delo_update},
		    {packages => 'arcboot',
		     path => '/etc/arcboot.conf',
		     list => \&delo_list,
		     update => \&delo_update},
		    {packages => 'extlinux',
		     path => extlinux_path(),
		     list => \&extlinux_list,
		     update => \&extlinux_update,
		     post_update => \&extlinux_post},
		    {packages => 'aboot',
		     path => '/etc/aboot.conf',
		     list => \&aboot_list,
		     update => \&aboot_update});

my %bdev_map = ();
my @matched_configs = ();
my %id_map;

sub scan_config_files {
    # Find all IDE/SCSI disks mentioned in configurations
    for my $config (@config_files) {
	# Is the file present?
	my $path = $config->{path};
	if (!defined($path)) {
	    next;
	}
	my $file = new FileHandle($path, 'r');
	if (!defined($file)) {
	    if ($! == POSIX::ENOENT) {
		next;
	    }
	    die $!;
	}

	# Are any of the related packages wanted or installed?
	my $wanted = 0;
	my $installed = 0;
	my $packages = $config->{packages};
	for (`dpkg-query 2>/dev/null --showformat '\${status}\\n' -W $packages`)
	{
	    $wanted = 1 if /^install /;
	    $installed = 1 if / installed\n$/;
	}
	if (!$wanted && !$installed) {
	    next;
	}

	my @matched_bdevs = ();

	for my $bdev (&{$config->{list}}($file)) {
	    if ($bdev =~ m{^/dev/(?:[hs]d[a-z]\d*|s(?:cd|r)\d+)$}) {
		$bdev_map{$bdev} = {};
		push @matched_bdevs, $bdev;
	    }
	}

	if (@matched_bdevs) {
	    push @matched_configs, {config => $config,
				    devices => \@matched_bdevs,
				    installed => $installed};
	}
    }

    my $fstab = new FileHandle('/etc/fstab', 'r');
    while (1) {
	my ($text, $bdev, $path, $type) = fstab_next($fstab);
	last unless defined($text);
	if (defined($type) && exists($bdev_map{$bdev})) {
	    $bdev_map{$bdev}->{path} = $path;
	    $bdev_map{$bdev}->{type} = $type;
	}
    }
    $fstab->close();
}

sub add_tag {
    # Map disks to labels/UUIDs and vice versa.  Include all disks in
    # the reverse mapping so we can detect ambiguity.
    my ($bdev, $name, $value) = @_;
    my $id = "$name=$value";
    push @{$id_map{$id}}, $bdev;
    if (exists($bdev_map{$bdev})) {
	$bdev_map{$bdev}->{$name} = $value;
	push @{$bdev_map{$bdev}->{ids}}, $id;
    }
}

sub scan_devices {
    for (`blkid -o device`) {
	chomp;
	my $bdev = $_;
	for (`blkid -o udev -s LABEL -s UUID '$bdev'`) {
	    if (/^ID_FS_(LABEL|UUID)_ENC=(.*)\n$/) {
		add_tag($bdev, $1, $2);
	    }
	}
    }

    # Discard all device ids that are ambiguous.
    for my $bdev (keys(%bdev_map)) {
	@{$bdev_map{$bdev}->{ids}} = grep({ $#{$id_map{$_}} == 0 }
					  @{$bdev_map{$bdev}->{ids}});
    }
}

sub assign_labels {
    my $hostname = (POSIX::uname())[1];

    # For all devices that have no alternate device ids, suggest labelling
    # them based on fstab or just using a generic label.
    for my $bdev (keys(%bdev_map)) {
	if ($#{$bdev_map{$bdev}->{ids}} >= 0) {
	    my $id = $bdev_map{$bdev}->{ids}->[0];
	} else {
	    my $type = $bdev_map{$bdev}->{type};
	    
	    if (!exists($label_types{$type})) {
		next;
	    }

	    my $label_len = $label_types{$type}->{len};
	    my $label;
	    use bytes; # string lengths are in bytes

	    if (defined($bdev_map{$bdev}->{path})) {
		# Convert path/type to label; prepend hostname if possible;
		# append numeric suffix if necessary.

		my $base;
		if ($bdev_map{$bdev}->{path} =~ m|^/|) {
		    $base = $bdev_map{$bdev}->{path};
		} else {
		    $base = $bdev_map{$bdev}->{type};
		}
		$base =~ s/[^\w]+/-/g;
		$base =~ s/^-//g;
		$base =~ s/-$//g;

		my $n = 0;
		my $suffix = '';
		do {
		    $label = "$hostname-$base$suffix";
		    if (length($label) > $label_len) {
			$label = substr($base, 0, $label_len - length($suffix))
			    . $suffix;
		    }
		    $n++;
		    $suffix = "-$n";
		} while (exists($id_map{"LABEL=$label"}));
	    } else {
		my $n = 0;
		my $suffix;
		do {
		    $n++;
		    $suffix = "-$n";
		    $label = substr($hostname, 0, $label_len - length($suffix))
			. $suffix;
		} while (exists($id_map{"LABEL=$label"}));
	    }

	    add_tag($bdev, 'LABEL', $label);
	    $bdev_map{$bdev}->{relabel} = 1;
	}
    }
}

sub relabel {
    for my $bdev (keys(%bdev_map)) {
	my $bdev_info = $bdev_map{$bdev};
	if ($bdev_info->{relabel}) {
	    my $relabel = $label_types{$bdev_info->{type}}->{relabel};
	    &{$relabel}($bdev, $bdev_info->{LABEL});
	}
    }
}

sub update_config {
    my %map;
    for my $bdev (keys(%bdev_map)) {
	$map{$bdev} = $bdev_map{$bdev}->{ids}->[0];
    }

    for my $match (@matched_configs) {
	# Generate a new config
	my $path = $match->{config}->{path};
	my $old = new FileHandle($path, 'r');
	my $new = new FileHandle("$path.new", POSIX::O_WRONLY | POSIX::O_CREAT,
				 0600);
	&{$match->{config}->{update}}($old, $new, \%map);
	$old->close();
	$new->close();

	# New config should have same permissions as the old
	my (undef, undef, $mode, undef, $uid, $gid) = stat($path) or die "$!";
	chown($uid, $gid, "$path.new") or die "$!";
	chmod($mode & 07777, "$path.new") or die "$!";

	# Back up the old config and replace with the new
	unlink("$path.old");
	link($path, "$path.old") or die "$!";
	rename("$path.new", $path) or die "$!";

	# If the package is installed, run the post-update function
	if ($match->{installed} && $match->{config}->{post_update}) {
	    &{$match->{config}->{post_update}}();
	}
    }
}

### main

scan_config_files();

if ($#matched_configs < 0) {
    exit 0;
}

my ($question, $answer, $ret, $seen);

$question = 'linux-image-2.6-common/disk-id-convert-auto';
($ret, $seen) = input('high', $question);
if ($ret && $ret != 30) {
    die "Error setting debconf question $question: $seen";
}
($ret, $seen) = go();
if ($ret && $ret != 30) {
    die "Error asking debconf question $question: $seen";
}
($ret, $answer) = get($question);
die "Error retrieving answer for $question: $answer" if $ret;

if ($answer eq 'true') {
    scan_devices();
    assign_labels();

    $question = 'linux-image-2.6-common/disk-id-convert-plan';
    ($ret, $seen) = subst($question, 'relabel',
			  join("\\n",
			       map({sprintf("%s: %s", $_, $bdev_map{$_}->{LABEL})}
				   grep({$bdev_map{$_}->{relabel}}
					keys(%bdev_map)))));
    die "Error setting debconf substitutions in $question: $seen" if $ret;
    ($ret, $seen) = subst($question, 'id_map',
			  join("\\n",
			       map({sprintf("%s: %s", $_, $bdev_map{$_}->{ids}->[0])}
				   grep({@{$bdev_map{$_}->{ids}}}
					keys(%bdev_map)))));
    die "Error setting debconf substitutions in $question: $seen" if $ret;
    ($ret, $seen) = subst($question, 'files',
			  join(', ',
			       map({$_->{config}->{path}} @matched_configs)));
    die "Error setting debconf substitutions in $question: $seen" if $ret;
    ($ret, $seen) = input('high', $question);
    if ($ret && $ret != 30) {
	die "Error setting debconf question $question: $seen";
    }
    ($ret, $seen) = go();
    if ($ret && $ret != 30) {
	die "Error asking debconf question $question: $seen";
    }
    ($ret, $answer) = get($question);
    die "Error retrieving answer for $question: $answer" if $ret;
    
    if ($answer ne 'true') {
	# TODO: go back to the auto/manual question or allow editing the plan
    } else {
	relabel();
	update_config();
    }
}

my @unconv_files = ();
for my $match (@matched_configs) {
    my @unconv_bdevs = grep({!exists($bdev_map{$_}->{ids}) ||
				 @{$bdev_map{$_}->{ids}} == 0}
			    @{$match->{devices}});
    if (@unconv_bdevs) {
	push @unconv_files, sprintf('%s: %s', $match->{config}->{path},
				    join(', ',@unconv_bdevs));
    }
}
if (@unconv_files) {
    $question = 'linux-image-2.6-common/disk-id-manual';
    ($ret, $seen) = subst($question, 'unconverted',
			  join("\\n", @unconv_files));
    die "Error setting debconf substitutions in $question: $seen" if $ret;
    ($ret, $seen) = input('high', $question);
    if ($ret && $ret != 30) {
	die "Error setting debconf note $question: $seen";
    }
    ($ret, $seen) = go();
    if ($ret && $ret != 30) {
	die "Error showing debconf note $question: $seen";
    }
}


-- 
Ben Hutchings
Humans are not rational beings; they are rationalising beings.

Attachment: signature.asc
Description: This is a digitally signed message part


Reply to: