#!/usr/bin/perl

# Copyright 2009-2011 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 ();
use UUID;

package DebianKernel::DiskId;

### utility

sub id_to_path {
    my ($id) = @_;
    $id =~ m|^/|
	or $id =~ s{^(LABEL|UUID)=}{'/dev/disk/by-' . lc($1) . '/'}e
	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_path {
    for ('/boot/grub', '/boot/boot/grub') {
	if (-d) {
	    return "$_/menu.lst";
	}
    }
    return undef;
}

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) = @$_;
	if (defined($name) && 
	    ($name eq 'kopt_2_6' ||
	     ($name eq 'kopt' && !exists($options{kopt_2_6})) ||
	     $name eq 'xenkopt')) {
	    if (defined(my $new_value = kernel_update($value, $map))) {
		$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)?$|);
		} elsif ($in_generic) {
		    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, $replace) = @_;
    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)?$|);
		} elsif ($in_generic) {
		    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)) {
		$new_value =~ s/\\/\\\\/g;
		$text = &{$replace}($name, $value, $new_value) ||
		    "\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_update {
    my ($old, $new, $map) = @_;
    _lilo_update($old, $new, $map, sub { return undef });
}

sub lilo_post {
    system('lilo');
}

### SILO

sub silo_post {
    system('silo');
}

### Yaboot

sub yaboot_post {
    system('ybin');
}

### ELILO

sub elilo_update {
    my ($old, $new, $map) = @_;
    # Work around bug #581173 - boot value must have no space before
    # and no quotes around it.
    sub replace {
	my ($name, $value, $new_value) = @_;
	return ($name eq 'boot') ? "# boot=$value\nboot=$new_value\n" : undef;
    }
    _lilo_update($old, $new, $map, \&replace);
}

sub elilo_post {
    system('elilo');
}

### extlinux

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

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

sub extlinux_old_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_new_list {
    my ($file) = @_;
    while (<$file>) {
	if (/^# ROOT=(.*)/) {
	    return kernel_list($1);
	}
    }
    return ();
}

sub extlinux_new_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');
}

# udev persistent-cd

sub udev_next {
    my ($file) = @_;
    my @results = ();

    # Based on parse_file() and get_key() in udev-rules.c
    while (1) {
	my $text = <$file>;
	last if !defined($text) || $text eq '';

	if ($text =~ /^\s*(?:#|$)/) {
	    push @results, [$text];
	} else {
	    my $end_pos = 0;
	    while ($text =~ /\G [\s,]* ((?:[^\s=+!:]|[+!:](?!=))+)
                         \s* ([=+!:]?=) "([^"]*)"/gx) {
		push @results, [$&, $1, $2, $3];
		$end_pos = pos($text);
	    }
	    push @results, [substr($text, $end_pos)];
	    last if $text !~ /\\\n$/;
	}
    }

    return @results;
}

sub udev_parse_symlink_rule {
    my ($path, $symlink);
    for (@_) {
	my ($text, $key, $op, $value) = @$_;
	next if !defined($key);
	if ($key eq 'ENV{ID_PATH}' && $op eq '==') {
	    $path = $value;
	} elsif ($key eq 'SYMLINK' && $op eq '+=') {
	    $symlink = $value;
	}
    }
    return ($path, $symlink);
}

# Find symlink rules using IDE device paths that aren't matched by rules
# using the corresponding SCSI device path.  Return an array containing
# the corresponding path for each rule where this is the case and undef
# for all other rules.
sub udev_cd_find_unmatched_ide_rules {
    my ($file) = @_;
    my %wanted_rule;
    my @unmatched;
    my $i = 0;

    while (1) {
	my @keys = udev_next($file);
	last if $#keys < 0;

	my ($path, $symlink) = udev_parse_symlink_rule(@keys);
	if (defined($path) && defined($symlink)) {
	    if ($path =~ /-ide-\d+:\d+$/) {
		# libata uses the PATA controller and device numbers
		# as SCSI host number and bus id.  Channel number and
		# LUN are always 0.  The parent device path should
		# stay the same.
		$path =~ s/-ide-(\d+):(\d+)$/-scsi-$1:0:$2:0/;
		my $rule_key =  $path . ' ' . $symlink;
		if (!exists($wanted_rule{$rule_key})) {
		    $wanted_rule{$rule_key} = $i;
		    $unmatched[$i] = $path;
		}
	    } elsif ($path =~ /-scsi-\d+:\d+:\d+:\d+$/) {
		my $rule_key =  $path . ' ' . $symlink;
		my $j = $wanted_rule{$rule_key};
		if (defined($j) && $j >= 0) {
		    $unmatched[$j] = undef;
		}
		$wanted_rule{$rule_key} = -1;
	    }
	}

	++$i;
    }

    return @unmatched;
}

sub udev_cd_needs_update {
    my ($file) = @_;
    my %paths;
    for (udev_cd_find_unmatched_ide_rules($file)) {
	if (defined($_)) {
	    $paths{$_} = 1;
	}
    }
    return join('\n', map({"+ PATH=$_"} keys(%paths)));
}

sub udev_cd_update {
    my ($old, $new) = @_; # ignore map

    # Find which rules we will need to copy and edit, then rewind
    my @unmatched = udev_cd_find_unmatched_ide_rules($old);
    $old->seek(0, 0);

    my $i = 0;
    while (1) {
	my @keys = udev_next($old);
	last if $#keys < 0;

	my $old_text = '';
	my $new_text = '';

	for (@keys) {
	    my ($text, $key, $op, $value) = @$_;
	    $old_text .= $text;
	    next unless defined($unmatched[$i]) && defined($key);

	    if ($key eq 'ENV{ID_PATH}' && $op eq '==') {
		my $value = $unmatched[$i];
		$new_text .= ", $key$op\"$value\"";
	    } else {
		$new_text .= $text;
	    }
	}

	$new->print($old_text);
	if ($unmatched[$i]) {
	    $new->print($new_text . "\n");
	}

	++$i;
    }
}

# initramfs-tools resume

sub initramfs_resume_list {
    my ($file) = @_;
    my @results = ();

    while (1) {
	my ($text, $name, $value) = shellvars_next($file);
	last unless defined($text);
	if (defined($name) && $name eq 'RESUME') {
	    $results[0] = $value;
	}
    }

    return @results;
}

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

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

# uswsusp resume

sub uswsusp_next {
    # Based on parse_line() in config_parser.c

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

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

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

    if ($text =~ /^([\w ]*\w)[ \t]*[:=][ \t]*(.+)$/) {
	return ($text, $1, $2);
    } else {
	return ($text);
    }
}

sub uswsusp_resume_list {
    my ($file) = @_;
    my @results = ();

    while (1) {
	my ($text, $name, $value) = uswsusp_next($file);
	last unless defined($text);
	if (defined($name) && $name eq 'resume device') {
	    $results[0] = $value;
	}
    }

    return @results;
}

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

    while (1) {
	my ($text, $name, $value) = uswsusp_next($old);
	last unless defined($text);
	if (defined($name) && $name eq 'resume device' &&
	    defined(my $new_value = $map->{$value})) {
	    $text =~ s/^/# /gm;
	    $text .= sprintf("%s = %s\n", $name, id_to_path($new_value));
	}
	$new->print($text);
    }
}

# cryptsetup

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

    my $line = $text;
    if ($line =~ /^\s*(#|$)/) {
	return ($text);
    } else {
	$line =~ s/\s*$//;
	$line =~ s/^\s*//;
	return ($text, split(/\s+/, $line, 4));
    }
}

sub cryptsetup_list {
    my ($file) = @_;
    my (@results) = ();

    while (1) {
	my ($text, undef, $src) = cryptsetup_next($file);
	last unless defined($text);
	if (defined($src)) {
	    push @results, $src;
	}
    }

    return @results;
}

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

    while (1) {
	my ($text, $dst, $src, $key, $opts) = cryptsetup_next($old);
	last unless defined($text);
	if (defined($src) && defined($map->{$src})) {
	    $text = "# $text" .
		join(' ', $dst, $map->{$src}, $key, $opts) . "\n";
	}
	$new->print($text);
    }
}

# hdparm

sub hdparm_list {
    my ($file) = @_;
    my (@results) = ();

    # I really can't be bothered to parse this mess.  Just see if
    # there's anything like a device name on a non-comment line.
    while (<$file>) {
	if (!/^\s*#/) {
	    push @results, grep({m|^/dev/|} split(/\s+/));
	}
    }

    return @results;
}

### mdadm

sub mdadm_list {
    my ($file) = @_;
    my (@results) = ();

    while (<$file>) {
	# Look for DEVICE (case-insensitive, may be abbreviated to as
	# little as 3 letters) followed by a whitespace-separated list
	# of devices (or wildcards, or keywords!).  Ignore comments
	# (hash preceded by whitespace).
	if (/^DEV(?:I(?:C(?:E)?)?)?[ \t]*((?:[^ \t]|[ \t][^#])*)/i) {
	    push @results, split(/[ \t]+/, $1);
	}
    }

    return @results;
}

### list of all configuration files and functions

my @config_files = ({packages => 'mount',
		     path => '/etc/fstab',
		     list => \&fstab_list,
		     update => \&fstab_update},
		    {packages => 'grub grub-legacy',
		     path => grub1_path(),
		     list => \&grub1_list,
		     update => \&grub1_update,
		     post_update => \&grub1_post,
		     is_boot_loader => 1},
		    {packages => 'grub-common',
		     path => '/etc/default/grub',
		     list => \&grub2_list,
		     update => \&grub2_update,
		     post_update => \&grub2_post,
		     is_boot_loader => 1},
		    {packages => 'lilo',
		     path => '/etc/lilo.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     post_update => \&lilo_post,
		     is_boot_loader => 1},
		    {packages => 'silo',
		     path => '/etc/silo.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     post_update => \&silo_post,
		     is_boot_loader => 1},
		    {packages => 'quik',
		     path => '/etc/quik.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     is_boot_loader => 1},
		    {packages => 'yaboot',
		     path => '/etc/yaboot.conf',
		     list => \&lilo_list,
		     update => \&lilo_update,
		     post_update => \&yaboot_post,
		     is_boot_loader => 1},
		    {packages => 'elilo',
		     path => '/etc/elilo.conf',
		     list => \&lilo_list,
		     update => \&elilo_update,
		     post_update => \&elilo_post,
		     is_boot_loader => 1},
		    {packages => 'extlinux',
		     path => extlinux_old_path(),
		     list => \&extlinux_old_list,
		     update => \&extlinux_old_update,
		     post_update => \&extlinux_post,
		     is_boot_loader => 1},
		    {packages => 'extlinux',
		     path => '/etc/default/extlinux',
		     list => \&extlinux_new_list,
		     update => \&extlinux_new_update,
		     post_update => \&extlinux_post,
		     is_boot_loader => 1},
		    {packages => 'udev',
		     path => '/etc/udev/rules.d/70-persistent-cd.rules',
		     needs_update => \&udev_cd_needs_update,
		     update => \&udev_cd_update},
		    {packages => 'initramfs-tools',
		     path => '/etc/initramfs-tools/conf.d/resume',
		     list => \&initramfs_resume_list,
		     update => \&initramfs_resume_update,
		     # udev will source all files in this directory,
		     # with few exceptions.  Such as including a '^'.
		     suffix => '^old'},
		    {packages => 'uswsusp',
		     path => '/etc/uswsusp.conf',
		     list => \&uswsusp_resume_list,
		     update => \&uswsusp_resume_update},
		    {packages => 'cryptsetup',
		     path => '/etc/crypttab',
		     list => \&cryptsetup_list,
		     update => \&cryptsetup_update},
		    # mdadm.conf requires manual update because it may
		    # contain wildcards.
		    {packages => 'mdadm',
		     path => '/etc/mdadm/mdadm.conf',
		     list => \&mdadm_list},
		    # hdparm.conf requires manual update because it
		    # (1) refers to whole disks (2) might not work
		    # properly with the new drivers (3) is in a very
		    # special format.
		    {packages => 'hdparm',
		     path => '/etc/hdparm.conf',
		     list => \&hdparm_list});

### Filesystem labels and UUIDs

sub ext2_set_label {
    my ($bdev, $label) = @_;
    system('tune2fs', '-L', $label, $bdev) == 0 or die "tune2fs failed: $?";
}
sub ext2_set_uuid {
    my ($bdev, $uuid) = @_;
    system('tune2fs', '-U', $uuid, $bdev) == 0 or die "tune2fs failed: $?";
}

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

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

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

sub reiserfs_set_label {
    my ($bdev, $label) = @_;
    system('reiserfstune', '--label', $label, $bdev)
	or die "reiserfstune failed: $?";
}
sub reiserfs_set_uuid {
    my ($bdev, $uuid) = @_;
    system('reiserfstune', '--uuid', $uuid, $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_UUID_OFFSET => 1036, SWAP_UUID_LEN => 16,
	       SWAP_LABEL_OFFSET => 1052, SWAP_LABEL_LEN => 16 };
sub _swap_set_field {
    my ($bdev, $offset, $value) = @_;
    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 field
    POSIX::lseek($fd, $offset, POSIX::SEEK_SET);
    $length = POSIX::write($fd, $value, length($value));
    if (!defined($length) || $length != length($value)) {
	my $error = "$!";
	POSIX::close($fd);
	die $error;
    }

    POSIX::close($fd);
}
sub swap_set_label {
    my ($bdev, $label) = @_;
    _swap_set_field($bdev, SWAP_LABEL_OFFSET, pack('Z' . SWAP_LABEL_LEN, $label));
}
sub swap_set_uuid {
    my ($bdev, $uuid) = @_;
    my $uuid_bin;
    if (UUID::parse($uuid, $uuid_bin) != 0 ||
	length($uuid_bin) != SWAP_UUID_LEN) {
	die "internal error: invalid UUID string";
    }
    _swap_set_field($bdev, SWAP_UUID_OFFSET, $uuid_bin);
}

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

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

my %filesystem_types = (
    ext2     => { label_len => 16,             set_label => \&ext2_set_label,
		  set_uuid  => \&ext2_set_uuid },
    ext3     => { label_len => 16,             set_label => \&ext2_set_label,
		  set_uuid  => \&ext2_set_uuid },
    ext4     => { label_len => 16,             set_label => \&ext2_set_label,
		  set_uuid  => \&ext2_set_uuid },
    jfs      => { label_len => 16,             set_label => \&jfs_set_label,
		  set_uuid  => \&jfs_set_uuid },
    msdos    => { label_len => 11,             set_label => \&fat_set_label },
    ntfs     => { label_len => 128,            set_label => \&ntfs_set_label },
    reiserfs => { label_len => 16,             set_label => \&reiserfs_set_label,
		  set_uuid  => \&reiserfs_set_uuid },
    swap     => { label_len => SWAP_LABEL_LEN, set_label => \&swap_set_label,
		  set_uuid  => \&swap_set_uuid },
    ufs      => { label_len => 32,             set_label => \&ufs_set_label },
    vfat     => { label_len => 11,             set_label => \&fat_set_label },
    xfs      => { label_len => 12,             set_label => \&xfs_set_label,
		  set_uuid  => \&xfs_set_uuid }
    );

my %bdev_map;
my %id_map;

sub scan_config_files {
    my $bdev_regex = shift;
    my @configs;

    # Find all matching devices 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 $unpacked = 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$/;
	    $unpacked = 1 if / (installed|unpacked)\n$/;
	}
	if (!$wanted && !$unpacked) {
	    next;
	}

	my @matched_bdevs = ();
	my $id_map_text;
	my $needs_update;

	if (exists($config->{needs_update})) {
	    $id_map_text = &{$config->{needs_update}}($file);
	    $needs_update = defined($id_map_text) && $id_map_text ne '';
	} elsif (exists($config->{list})) {
	    for my $bdev (&{$config->{list}}($file)) {
		# Check whether the device name matches the given
		# regex.  Also check that the device node exists,
		# unless the name is a wildcard.
		if ($bdev =~ $bdev_regex && ($bdev =~ m/[\?\*]/ || -b $bdev)) {
		    $bdev_map{$bdev} = {};
		    push @matched_bdevs, $bdev;
		}
	    }
	    $needs_update = @matched_bdevs > 0;
	} else {
	    # Needs manual update
	    $needs_update = 1;
	}

	push @configs, {config => $config,
			devices => \@matched_bdevs,
			id_map_text => $id_map_text,
			installed => $installed,
			unpacked => $unpacked,
			needs_update => $needs_update};
    }

    my $fstab = new FileHandle('/etc/fstab', 'r') or die "$!";
    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();

    return @configs;
}

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, $new) = @_;
    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;
    }
    if ($new) {
	$bdev_map{$bdev}->{new_id} = $id;
    }
}

sub scan_devices {
    my $id_command;
    if (-x '/sbin/vol_id') {
	$id_command = '/sbin/vol_id';
    } else {
	$id_command = 'blkid -o udev -s LABEL -s UUID -s TYPE';
    }
    for (`blkid -o device`) {
	chomp;
	my $bdev = $_;
	for (`$id_command '$bdev'`) {
	    if (/^ID_FS_(LABEL|UUID)_ENC=(.+)\n$/) {
		add_tag($bdev, $1, $2);
	    } elsif (/^ID_FS_TYPE=(.+)\n$/ && exists($bdev_map{$bdev})) {
		$bdev_map{$bdev}->{type} //= $1;
	    }
	}
    }

    # Discard UUIDs for LVM2 PVs, as we assume there are symlinks for all
    # UUIDs under /dev/disk/by-uuid and this is not true for PVs.
    # Discard all labels and UUIDs(!) that are ambiguous.
    # Discard all labels with 'unsafe' characters (escaped by blkid using
    # backslashes) as they will not be usable in all configuration files.
    # Similarly for '#' which blkid surprisingly does not consider unsafe.
    # Sort each device's IDs in reverse lexical order so that UUIDs are
    # preferred.
    for my $bdev (keys(%bdev_map)) {
	if (!defined($bdev_map{$bdev}->{type}) ||
	    $bdev_map{$bdev}->{type} eq 'LVM2_member') {
	    @{$bdev_map{$bdev}->{ids}} = ();
	} else {
	    @{$bdev_map{$bdev}->{ids}} =
		sort({$b cmp $a}
		     grep({ @{$id_map{$_}} == 1 && $_ !~ /[\\#]/ }
			  @{$bdev_map{$bdev}->{ids}}));
	}
    }

    # Add persistent aliases for CD/DVD/BD drives
    my $cd_rules =
	new FileHandle('/etc/udev/rules.d/70-persistent-cd.rules', 'r');
    while (defined($cd_rules)) {
	my @keys = udev_next($cd_rules);
	last if $#keys < 0;

	my ($path, $symlink) = udev_parse_symlink_rule(@keys);
	if (defined($path) && defined($symlink)) {
	    $symlink =~ s{^(?!/)}{/dev/};
	    my $bdev = readlink($symlink) or next;
	    $bdev =~ s{^(?!/)}{/dev/};
	    if (exists($bdev_map{$bdev})) {
		push @{$bdev_map{$bdev}->{ids}}, $symlink;
	    }
	}
    }
}

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

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

	my $type = $bdev_map{$bdev}->{type};
	next unless defined($type) && exists($filesystem_types{$type});

	if (defined($filesystem_types{$type}->{set_uuid})) {
	    my ($uuid_bin, $uuid);
	    UUID::generate($uuid_bin);
	    UUID::unparse($uuid_bin, $uuid);
	    add_tag($bdev, 'UUID', $uuid, 1);
	    next;
	}

	my $label_len = $filesystem_types{$type}->{label_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, 1);
    }
}

sub set_new_ids {
    for my $bdev (keys(%bdev_map)) {
	my $bdev_info = $bdev_map{$bdev};
	if ($bdev_info->{new_id}) {
	    my ($name, $value) = split(/=/, $bdev_info->{new_id}, 2);
	    my $setter;
	    if ($name eq 'UUID') {
		$setter = $filesystem_types{$bdev_info->{type}}->{set_uuid};
	    } elsif ($name eq 'LABEL') {
		$setter = $filesystem_types{$bdev_info->{type}}->{set_label};
	    }
	    defined($setter) or die "internal error: invalid new_id type";
	    &{$setter}($bdev, $value);
	}
    }
}

sub update_config {
    my $map = shift;

    for my $match (@_) {
	# Generate a new config
	my $path = $match->{config}->{path};
	my $old = new FileHandle($path, 'r') or die "$!";
	my $new = new FileHandle("$path.new", POSIX::O_WRONLY | POSIX::O_CREAT,
				 0600)
	    or die "$!";
	&{$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
	my $old_path = $path . ($match->{config}->{suffix} || '.old');
	unlink($old_path);
	link($path, $old_path) or die "$!";
	rename("$path.new", $path) or die "$!";

	# If the package is installed, run the post-update function.
	# If the package is only unpacked, assume that its own postinst
	# will cover this.
	if ($match->{installed} && $match->{config}->{post_update}) {
	    &{$match->{config}->{post_update}}();
	}
    }
}

sub update_all {
    # The update process may be aborted if a command fails, but we now
    # want to recover and ask the user what to do.  We can use 'do' to
    # prevent 'die' from exiting the process, but we also need to
    # capture and present error messages using debconf as they may
    # otherwise be hidden.  Therefore, we fork and capture stdout and
    # stderr from the update process in the main process.
    my $pid = open(PIPE, '-|');
    return (-1, '') unless defined $pid;

    if ($pid == 0) {
	# Complete redirection
	# </dev/null
	POSIX::close(0);
	POSIX::open('/dev/null', POSIX::O_RDONLY) or die "$!";
	# 2>&1
	POSIX::dup2(1, 2) or die "$!";

	# Do the update
	set_new_ids();
	update_config(@_);
	exit;
    } else {
	my @output = ();
	while (<PIPE>) {
	    push @output, $_;
	}
	close(PIPE);
	return ($?, join('', @output));
    }
}

sub transition {
    use Debconf::Client::ConfModule ':all';

    my $bdev_regex = shift;

retry:
    %bdev_map = ();
    %id_map = ();

    my @found_configs = scan_config_files($bdev_regex);
    my @matched_configs = grep({$_->{needs_update}} @found_configs);
    my @auto_configs = grep({defined($_->{config}->{update})} @matched_configs);
    my $found_boot_loader =
	grep({$_->{config}->{is_boot_loader} && $_->{unpacked}} @found_configs);
    my %update_map = ();

    # We can skip all of this if we didn't find any configuration
    # files that need conversion and we found the configuration file
    # for an installed boot loader.
    if (!@matched_configs && $found_boot_loader) {
	return;
    }

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

    $question = 'linux-base/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 (@auto_configs && $answer eq 'true') {
	scan_devices();
	assign_new_ids();

	# Construct the device ID update map
	for my $bdev (keys(%bdev_map)) {
	    if (@{$bdev_map{$bdev}->{ids}}) {
		$update_map{$bdev} = $bdev_map{$bdev}->{ids}->[0];
	    }
	}

	# Weed out configurations which will be unaffected by this
	# mapping or by a custom mapping described in id_map_text.
	@auto_configs = grep({ defined($_->{id_map_text}) ||
				   grep({exists($update_map{$_})}
					@{$_->{devices}}) }
			     @auto_configs);
    }

    if (@auto_configs && $answer eq 'true') {
	if (grep({$bdev_map{$_}->{new_id}} keys(%bdev_map))) {
	    $question = 'linux-base/disk-id-convert-plan';
	    ($ret, $seen) = subst($question, 'relabel',
				  join("\\n",
				       map({sprintf("%s: %s",
						    $_, $bdev_map{$_}->{new_id})}
					   grep({$bdev_map{$_}->{new_id}}
						keys(%bdev_map)))));
	    die "Error setting debconf substitutions in $question: $seen" if $ret;
	} else {
	    $question = 'linux-base/disk-id-convert-plan-no-relabel';
	}
	($ret, $seen) = subst($question, 'id_map',
			      join("\\n",
				   map({sprintf("%s: %s", $_, $update_map{$_})}
				       keys(%update_map)),
				   grep({defined}
					map({$_->{id_map_text}} @auto_configs))));
	die "Error setting debconf substitutions in $question: $seen" if $ret;
	($ret, $seen) = subst($question, 'files',
			      join(', ',
				   map({$_->{config}->{path}} @auto_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 eq 'true') {
	    my ($rc, $output) = update_all(\%update_map, @auto_configs);
	    if ($rc != 0) {
		# Display output of update commands
		$question = 'linux-base/disk-id-update-failed';
		$output =~ s/\n/\\n/g;
		($ret, $seen) = subst($question, 'output', $output);
		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";
		}

		# Mark previous questions as unseen
		fset('linux-base/disk-id-convert-auto', 'seen', 'false');
		fset('linux-base/disk-id-convert-plan', 'seen', 'false');
		fset('linux-base/disk-id-convert-plan-no-relabel', 'seen',
		     'false');
		goto retry;
	    }
	}
    }

    my @unconv_files = ();
    for my $match (@matched_configs) {
	if (!defined($match->{config}->{update})) {
	    push @unconv_files, $match->{config}->{path};
	} else {
	    my @unconv_bdevs = grep({!exists($update_map{$_})}
				    @{$match->{devices}});
	    if (@unconv_bdevs) {
		push @unconv_files, sprintf('%s: %s', $match->{config}->{path},
					    join(', ',@unconv_bdevs));
	    }
	}
    }
    if (@unconv_files) {
	$question = 'linux-base/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";
	}
    }

    # Also note whether some (unknown) boot loader configuration file
    # must be manually converted.
    if (!$found_boot_loader) {
	$question = 'linux-base/disk-id-manual-boot-loader';
	($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";
	}
    }
}

package DebianKernel::BootloaderConfig;

my %default_bootloader = (amd64 => 'lilo',
			  i386  => 'lilo',
			  ia64  => 'elilo',
			  s390  => 'zipl');

sub check {
    use Debconf::Client::ConfModule ':all';

    my ($deb_arch) = @_;

    # Is there an historical 'default' boot loader for this architecture?
    my $loader_exec = $default_bootloader{$deb_arch};
    return unless defined($loader_exec);

    # Is the boot loader installed?
    my ($loaderloc) = grep(-x, map("$_/$loader_exec",
				   map({ length($_) ? $_ : "." }
				       split(/:/, $ENV{PATH}))));
    return unless defined($loaderloc);

    # Is do_bootloader explicitly set one way or the other?
    my $do_bootloader;
    if (my $conf = new FileHandle('/etc/kernel-img.conf', 'r')) {
	while (<$conf>) {
	    $do_bootloader = 0 if /^\s*do_bootloader\s*=\s*(no|false|0)\s*$/i;
	    $do_bootloader = 1 if /^\s*do_bootloader\s*=\s*(yes|true|1)\s*$/i;
	}
	$conf->close();
    }
    return if defined($do_bootloader);

    # Warn the user that do_bootloader is disabled by default.
    my ($question, $ret, $seen);
    $question = "linux-base/do-bootloader-default-changed";
    ($ret,$seen) = input('high', "$question");
    die "Error setting debconf question $question: $seen" if $ret && $ret != 30;
    ($ret,$seen) = go();
    die "Error asking debconf question $question: $seen" if $ret && $ret != 30;
}

package main;

capb('escape');

sub version_lessthan {
    my ($left, $right) = @_;
    return system('dpkg', '--compare-versions', $left, 'lt', $right) == 0;
}

# No upgrade work is necessary during a fresh system installation.
# But since linux-base is a new dependency of linux-image-* and did
# not exist until needed for the libata transition, we cannot simply
# test whether this is a fresh installation of linux-base.  Instead,
# we test:
# - does /etc/fstab exist yet (this won't even work without it), and
# - are any linux-image-* packages installed yet?
sub is_fresh_installation {
    if (-f '/etc/fstab') {
	for (`dpkg-query 2>/dev/null --showformat '\${status}\\n' -W 'linux-image-*'`) {
	    return 0 if / installed\n$/;
	}
    }
    return 1;
}

my $deb_arch = `dpkg --print-architecture`;
chomp $deb_arch;

my $reconfigure = ($ARGV[0] eq 'reconfigure' ||
		   defined($ENV{DEBCONF_RECONFIGURE}));
if ($deb_arch ne 's390' && ($reconfigure || !is_fresh_installation())) {
    my @bdev_regex = ();

    my $libata_transition_ver =
	($deb_arch eq 'i386' || $deb_arch eq 'amd64') ? '2.6.32-10' : '2.6.32-11';
    if ($reconfigure || version_lessthan($ARGV[1], $libata_transition_ver)) {
	# Match standard IDE and SCSI device names, plus wildcards
	# in disk device names to allow for mdadm insanity.
	push @bdev_regex, '[hs]d[a-z\?\*][\d\?\*]*$';
	push @bdev_regex, 's(?:cd|r)\d+$';
    }

    # hpsa took over some controllers from cciss in 2.6.37, so their
    # targets are also treated (and named) like SCSI devices now.
    if ($reconfigure || version_lessthan($ARGV[1], '3')) {
	push @bdev_regex, 'cciss/';
	push @bdev_regex, 'sd[a-z\?\*][\d\?\*]*$';
    }

    if (@bdev_regex) {
	DebianKernel::DiskId::transition('^/dev/(?:' .
					 join('|', @bdev_regex) . ')');
    }
}

if (!is_fresh_installation() && version_lessthan($ARGV[1], '2.6.32-18')) {
    DebianKernel::BootloaderConfig::check($deb_arch);
}

exec("set -e\nset -- @ARGV\n" . << 'EOF');
#DEBHELPER#
EOF
