All pastes #219709 Raw Edit

Something

public text v1 · immutable
#219709 ·published 2006-10-24 20:41 UTC
rendered paste body
#!/usr/bin/perl -w

use strict;
use Getopt::Long;

my $CONFIG = "/etc/mdadm/mdadm.conf";
my $STATE = "/proc/mdstat";
my $SCSI = "/proc/scsi/scsi";
my $SCSIINFO = "/usr/sbin/scsiinfo";
my $SFDISK = "/sbin/sfdisk";
my $MDADM = "/sbin/mdadm";
my $GRUB = "/sbin/grub";

my @BOOTDRIVES = ("sda", "sdb");
my $TMPPATH = "/root/fixraid";
my $TMPSFDISK = "$TMPPATH.sfdisk.";
my $TMPSCSI = "$TMPPATH.scsi.";
my $TMPGRUB = "$TMPPATH.grub.";

my %mdConfig;
my %mdState;
my %scsi;
my %args;

my ($remove, $add, $fix, $test);

GetOptions('remove=s'	=> \$remove,
	   'add=s'	=> \$add,
	   'fix'	=> \$fix);

SanityCheck();
ReadConfig();
ReadState();
ReadSCSI();

if (defined($remove)) {
	Remove($remove);
} elsif (defined($add)) {
	Add($add);
} elsif (defined($fix)) {
	Fix($fix);
} else {
	ShowFailed();
}

Exit(0);

sub ReadConfig {
	# Build matrix of md's and the sd's that are supposed to be 
	# part of them. Store RAID level as hash value
	my ($md, $raid, @parts);
	undef(%mdConfig);
	open(CONFIG, $CONFIG);
	while (my $line = <CONFIG>) {
		if ($line =~ m/^ARRAY (\S+) level=(raid\d+) .*$/) {
			$md = $1;
			$raid = $2;
			$line = <CONFIG>;
			$line =~ s/^\s+devices=(.*)\n$/$1/;
			@parts = split(/,/, $line);
			@parts = sort(@parts);
			foreach my $sd (@parts) {
				$mdConfig{$md}{$sd} = $raid;
			}
		}
	}
	close(CONFIG);
}

sub ReadState {
	# Build matrix of md's and the sd's that are actually present, 
	# noting their state. (sd could be present but failed.)
	my ($md, @parts);
	undef(%mdState);
	open(STATE, $STATE);
	while (my $line = <STATE>) {
		if ($line =~ m/^(md\d+).*$/) {
			$md = "/dev/$1";
			if ($line =~ m/^.* active raid\d+ (.*)\n$/) {
				$line = $1;
				@parts = split(/\s+/, $line);
				@parts = sort(@parts);
				foreach my $sd (@parts) {
					if ($sd =~ m/\(F\)/) {
						$sd =~ s/\[.*$//;
						$sd = "/dev/$sd";
						$mdState{$md}{$sd} = "F";
					} else {
						$sd =~ s/\[.*$//;
						$sd = "/dev/$sd";
						$mdState{$md}{$sd} = "U";
					}
				}
			} else {
				die("ReadState() confused by:\n$line");
			}
		}
	}
	close(STATE);
}

sub ReadSCSI {
	# Build hash of drive letter ID's and numeric SCSI paths
	# Devices may not be in order in /proc/scsi/scsi
	my (@drives, @devices, $bus, $chan, $id, $lun);
	my $i = "0";

	undef(%scsi);

	open(SCSI, $SCSI);
	local $/ = "Host:";
	while (<SCSI>) {
		s/.*?
		    scsi	(\d+).+?
		    Channel:\s+	(\d+).+?
		    Id:\s+	(\d+).+?
		    Lun:\s+	(\d+).+?
		    Type:\s+	(\S+).*
		/$1 $2 $3 $4/sgx;
		push(@devices, $_) if (defined($5) && $5 eq 'Direct-Access');
	}
	close(SCSI);

	@drives = split(' ', qx($SCSIINFO -l));
	@devices = sort(@devices); 
	foreach my $path (@devices) {
		$scsi{$drives[$i]} = $path;
		$i++;
	}
}

sub ShowFailed {
	# Display md's with missing or failed sd's, and RAID level
	my $fail = 0;
	foreach my $md (keys(%mdConfig)) {
		foreach my $sd (keys(%{$mdConfig{$md}})) {
			if (IsDown($sd) || IsFailed($sd)) {
				print("$md $mdConfig{$md}{$sd} $sd\n");
				$fail = 1;
			}
		}
	}
	Exit(1) if ($fail);
}

sub Failures {
	ReadState();
	my @failures;
	foreach my $md (keys(%mdConfig)) {
		foreach my $sd (keys(%{$mdConfig{$md}})) {
			if (IsDown($sd) || IsFailed($sd)) {
			    	push(@failures, $sd);
			}
		}
	}
	return(@failures);
}
		

sub Remove {
	# Remove the specified sd from any md's it is part of, store SCSI path
	# and partition information, and remove the sd from /proc/scsi/scsi
	my $drive = shift();

	if (!IsPresentSCSI($drive)) {
		Exit(1, "/dev/$drive is not present in system");
	}

	PreRemoveCheck($drive);
	
	foreach my $md (keys(%mdState)) {
		foreach my $sd (keys(%{$mdState{$md}})) {
			if ($sd =~ m/$drive/) {
				if (IsUp($sd)) {
					RAIDSetFaulty($md, $sd);
				}
				RAIDHotRemove($md, $sd);
			}
		}
	}

	SaveDrive($drive);

	ReadState();
	if (IsPresentRAID($drive)) {
		Exit(1, "/dev/$drive RAID removal failed");
	}

	# Don't iterate through this hash, we know the hash key - ARGV STANDARD
	foreach my $sd (keys(%scsi)) {
		if ($sd =~ m/$drive/) {
			system("echo 'scsi remove-single-device $scsi{$sd}' " .
			    "> /proc/scsi/scsi");
			last;
		}
	}

	ReadSCSI();
	if (IsPresentSCSI($drive)) {
		Exit(2, "/dev/$drive SCSI removal failed");
	}
}

sub PreRemoveCheck {
	# Make sure drive to be removed is not part of a RAID0 array,
	# would not be the second missing element in a RAID5 array,
	# and isn't the last element in a RAID1 array.
	# 
	# This function could stand a rewrite.
	#
	my $drive = shift();

	foreach my $md (keys(%mdConfig)) {
		foreach my $sd (keys(%{$mdConfig{$md}})) {
			if ($sd =~ m/$drive/) {
				if ($mdConfig{$md}{$sd} eq 'raid0') {
					Exit(2, "Cannot remove /dev/$drive:\n" .
					    "$sd is device of raid0 array $md");
				} elsif ($mdConfig{$md}{$sd} eq 'raid1') {
					if (keys(%{$mdState{$md}}) < 2 && IsUp($sd)) {
						Exit(2, "Cannot remove /dev/$drive:\n" .
						    "$sd is final device of raid1 array $md");
					}
				} elsif ($mdConfig{$md}{$sd} eq 'raid5') {
					if ((keys(%{$mdConfig{$md}}) - keys(%{$mdState{$md}})) == 1 && IsUp($sd)) {
						Exit(2, "Cannot remove /dev/$drive:\n" .
						    "$sd is active device of raid5 array $md which is already missing one device.");
					}
				} else {
					Exit(2, "/dev/$drive is part of $mdConfig{$md}{$sd} array $md\n" .
					    "Don't know how to operate on $mdConfig{$md}{$sd} arrays");
				}
			}
		}
	}
}

sub Add {
	# Insert the specified sd in /proc/scsi/scsi, repartition, 
	# and insert sd into all appropriate md's
	my $drive = shift();

	if (!IsPresentSCSI($drive)) {
		ReinsertDrive($drive);
	}
	
	foreach my $md (keys(%mdConfig)) {
		foreach my $sd (keys(%{$mdConfig{$md}})) {
			if ($sd =~ m/$drive/) {
				if (IsFailed($sd)) {
					RAIDHotRemove($md, $sd);
					RAIDHotAdd($md, $sd);
				} elsif (IsDown($sd)) {
					RAIDHotAdd($md, $sd);
				}
			}
		}
	}

	ReadState();
	if (!IsPresentRAID($drive)) {
		Exit(2, "/dev/$drive RAID add failed");
	}
	Grub();
}

sub Fix {
	my ($md, $sd);
	foreach $sd (Failures()) {
		($md) = MemberOf($sd);
		RAIDHotRemove($md, $sd) if (IsFailed($sd));
		RAIDHotAdd($md, $sd);
	}
	if (Failures() == 0) {
		Exit(0);
	} else {
		print("Unable to resolve all failures\n");
		ShowFailed();
	}
}

sub Members {
	# Takes an md name and returns a list of sd's that 
	# are its devices
	my @members;
	my $md = shift();
	foreach my $sd (keys(%{$mdConfig{$md}})) {
		push(@members, $sd);
	}
	return(@members);
}

sub RAIDSetFaulty {
	my ($md, $sd) = @_;
	system("$MDADM $md -f $sd 2> /dev/null 1> /dev/null");
}

sub RAIDHotRemove {
	my ($md, $sd) = @_;
	system("$MDADM $md -r $sd 2> /dev/null 1> /dev/null");
}

sub RAIDHotAdd {
	my ($md, $sd) = @_;
	system("$MDADM $md -a $sd 2> /dev/null 1> /dev/null");
}

sub IsUp {
	my $sd = shift();
	my ($md) = MemberOf($sd);
	if (defined($mdState{$md}{$sd}) &&
	    $mdState{$md}{$sd} eq 'U') {
		return(1);
	}
	return(0);
}

sub IsDown {
	my $sd = shift();
	my ($md) = MemberOf($sd);
	if (!defined($mdState{$md}{$sd})) {
		return(1);
	}
	return(0);
}

sub IsFailed {
	my $sd = shift();
	my ($md) = MemberOf($sd);
	if (defined($mdState{$md}{$sd}) && 
	    $mdState{$md}{$sd} eq 'F') {
	    	return(1);
	}
	return(0);
}

sub MemberOf {
	# Takes an sd argument and returns the md it's a member of
	# If given an sd without partition (e.g., sda vs. sda1)
	# returns all md's the sd is a member of
	my @list;
	my $arg = shift();
	foreach my $md (keys(%mdConfig)) {
		foreach my $sd (keys(%{$mdConfig{$md}})) {
			if ($sd =~ m/$arg/) {
				push(@list, $md);
			}
		}
	}
	return(@list);
}

sub IsPresentSCSI {
	# Report presence of sd in /proc/scsi/scsi
	# ARGV STANDARD
	my $arg = shift();
	# Don't iterate through %scsi, we have the hash key - ARGV STANDARD
	foreach my $drive (keys(%scsi)) {
		if ($drive =~ m/$arg/) {
			return(1);
		}
	}
	return(0);
}

sub IsPresentRAID {
	# Report presence of sd, up or down, in any md
	# ARGV STANDARD
	my $arg = shift();
	foreach my $md (keys(%mdState)) {
		foreach my $sd (keys(%{$mdState{$md}})) {
			if ($sd =~ m/$arg/) {
				return(1);
			}
		}
	}
	return(0);
}

sub ReinsertDrive {
	# Reinsert drive in /proc/scsi/scsi and repartition drive using 
	# data stored by SaveDrive()
	my ($bus, $chan, $id, $lun);
	my $drive = shift();

	if (-r "$TMPSCSI$drive") {
		open(SCSI, "<", "$TMPSCSI$drive");
		($bus, $chan, $id, $lun) = split(/\s+/, <SCSI>);
		close(SCSI);
		unlink("$TMPSCSI$drive");
		qx(echo "scsi add-single-device $bus $chan $id $lun" > /proc/scsi/scsi);
		ReadSCSI();
		if (!IsPresentSCSI($drive)) {
			Exit(2, "Inserting $drive into SCSI chain failed");
		}
	} else { 
		Exit(2, "No information on how to reinsert /dev/$drive");
	}

	if (-r "$TMPSFDISK$drive") {
		if (system("$SFDISK /dev/$drive < $TMPSFDISK$drive " .
		    "2> /dev/null 1> /dev/null")) {
			Exit(1, "Failed to partition /dev/$drive");
		} else {
			unlink("$TMPSFDISK$drive");
		}
	} else {
		Exit(2, "No information on how to repartition /dev/$drive");
	}
}
	
sub SanityCheck {
	if (! -r $CONFIG || ! -r $STATE || ! -r $SCSI || ! -x $GRUB ||
	    ! -x $SCSIINFO || ! -x $SFDISK || ! -x $MDADM) {
		Exit(2, "Missing a required file or utility");
	}
}

sub SaveDrive {
	# Store partition table and SCSI path
	my $drive = shift();

	if (system("$SFDISK -d /dev/$drive 1> $TMPSFDISK$drive")) {
		unlink("$TMPSFDISK$drive");
		Exit(2, "Failed to dump existing partition map from /dev/$drive");
	}

	open(SCSI, ">", "$TMPSCSI$drive") or 
	    die("Couldn't open $TMPSCSI$drive for writing");
	print(SCSI $scsi{"/dev/$drive"});
	close(SCSI);
}

sub Grub {
	# Write grub boot labels to anything defined as a boot drive
	for (@BOOTDRIVES) {
		if (IsPresentSCSI($_)) {
			open(GRUB, ">", "$TMPGRUB$_") or 
			    die("Couldn't open $TMPGRUB$_ for writing");
			print(GRUB "device (hd0) /dev/$_\n" .
			    "root (hd0,0)\n" .
			    "setup (hd0)\n");
			close(GRUB);
			system("$GRUB < $TMPGRUB$_ 2> /dev/null 1> /dev/null");
			unlink("$TMPGRUB$_");
		}
	}
}

sub Exit {
	my ($exitVal, $errMsg) = @_;
	print("$errMsg\n") if defined($errMsg);
	exit($exitVal);
}