#!/usr/bin/perl # Copyright 2009-2010 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_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 "=" or 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'); } ### 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 => '/boot/grub/menu.lst', 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, 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 @configs; # 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 = (); 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)) { # Match standard IDE and SCSI device names, plus wildcards # in disk device names to allow for mdadm insanity. if ($bdev =~ m{^/dev/(?:[hs]d[a-z\?\*][\d\?\*]*| s(?:cd|r)\d+)$}x && ($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, 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 ($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 ($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 # &1 POSIX::dup2(1, 2) or die "$!"; # Do the update set_new_ids(); update_config(@_); exit; } else { my @output = (); while () { push @output, $_; } close(PIPE); return ($?, join('', @output)); } } sub transition { use Debconf::Client::ConfModule ':all'; retry: %bdev_map = (); %id_map = (); my @found_configs = scan_config_files(); 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} && $_->{installed}} @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 /do_bootloader\s*=\s*(no|false|0)\s*$/i; $do_bootloader = 1 if /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; if ($deb_arch ne 's390') { my $libata_transition_ver = ($deb_arch eq 'i386' || $deb_arch eq 'amd64') ? '2.6.32-10' : '2.6.32-11'; if ($ARGV[0] eq 'reconfigure' || defined($ENV{DEBCONF_RECONFIGURE}) || (!is_fresh_installation() && version_lessthan($ARGV[1], $libata_transition_ver))) { DebianKernel::DiskId::transition(); } } 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