#!/bin/perl
#
# This script is designed to identify hard drives and decide where and how to partition it for installation
# during a kickstart install.
#
# Exit codes;
# 0 - Success
# 1 - Target type not specified.
# 2 - Failed to find a drive to install on.
#
# NOTE: This is restricted to what is available during an anaconda install session. That is to say, bare
# minimum.
# TODO: If multiple matching drives are found (same medium and size, build an appropriate RAID array.
# TODO: in pre, wipefs on all disks to clear old LVM and DRBD data
#
use strict;
use warnings;
# Set to '1' for verbose output
my $debug = 0;
### NOTE: This must be set to 'striker', 'node' or 'dr'! Wither set '$type' or use the appropriate argument.
my $type = "";
if ((defined $ARGV[0]) && ((lc($ARGV[0]) eq "striker") or (lc($ARGV[0]) eq "node") or (lc($ARGV[0]) eq "dr")))
{
$type = $ARGV[0];
}
if ($type =~ /striker/i)
{
print "-=] Finding install drive(s) for a Striker dashboard.\n";
$type = "striker";
}
elsif ($type =~ /node/i)
{
print "-=] Finding install drive(s) for an Anvil! node.\n";
$type = "node";
}
elsif ($type =~ /dr/i)
{
print "-=] Finding install drive(s) for a DR (disaster recovery) host.\n";
$type = "dr";
}
else
{
print "
[ Error ] - Target type not specified!
Usage: ".$0." {striker,node,dr}
";
exit(1);
}
my $device = {};
# We might want to add HCTL (Host:Channel:Target:Lun for SCSI) and/or SUBSYSTEMS later
my $drives = {};
my $target = "";
my $lsblk = system_call("/bin/lsblk --bytes --paths --pairs --output NAME,RM,HOTPLUG,TYPE,SIZE,TRAN,ROTA");
foreach my $line (split/\n/, $lsblk)
{
### NOTE: If a drive has no transport, is not removable, but is hotplugable and the device path is
### mmcblk0, it is probably an SDCard. It doesn't seem to be a directly divinable state. We
### don't currently plan to use them, but it might come to pass later.
print __LINE__."; [ Debug ] - lsblk: [".$line."]\n" if $debug;
my ($path, $removable, $hotplug, $type, $size, $transport, $rotational) = ($line =~ /NAME="(.*?)" RM="(\d)" HOTPLUG="(\d)" TYPE="(.*?)" SIZE="(\d+)" TRAN="(.*?)" ROTA="(\d)"/);
print __LINE__."; [ Debug ] - Device: [".$path."], type: [".$type."], remvoable? [".$removable."], hotplug? [".$hotplug."], rotational? [".$rotational."], transport: [".$transport."], size: [".$size."]\n" if $debug;
# Skip 'zramX' devices
next if ($path =~ /^\/dev\/zram\d/);
# Skip removable disks and anything that just isn't a disk at all.
next if (($removable) or ($hotplug) or ($type ne "disk"));
$device->{$path} = {
type => $type,
size => $size,
transport => $transport,
rotational => $rotational,
};
my $hr_size = hr_size($device->{$path}{size});
$device->{$path}{hr_size} = $hr_size;
if ($device->{$path}{rotational})
{
if (not $device->{$path}{transport})
{
print "Analyzing platter or virtual drive: [".$path."] of the size: [".$device->{$path}{size}." (".$device->{$path}{hr_size}.")]\n";
}
else
{
print "Analyzing platter drive: [".$path."], using the transport: [".$device->{$path}{transport}."], of the size: [".$device->{$path}{size}." (".$device->{$path}{hr_size}.")]\n";
}
}
else
{
print "Analyzing solid-state drive: [".$path."], using the transport: [".$device->{$path}{transport}."], of the size: [".$device->{$path}{size}." (".$device->{$path}{hr_size}.")]\n";
}
if (not exists $drives->{by_hr_size}{$hr_size})
{
$drives->{by_hr_size}{$hr_size} = [];
}
push @{$drives->{by_hr_size}{$hr_size}}, $path;
}
### Usage selection priority
# on Striker, we'll simply use whatever is the biggest avalable drive.
# on Node and DR, we'll prefer slowest first (rotational, sata before nvme/scsi), and smallest second.
my $use_drive = "";
if ($type eq "striker")
{
my $biggest_size = 0;
foreach my $path (sort {$a cmp $b} keys %{$device})
{
print __LINE__."; [ Debug ] - path: [".$path."], ${path}::size: [".$device->{$path}{size}." (".hr_size($device->{$path}{size}).")] < biggest_size: [".$biggest_size." (".hr_size($biggest_size).")]\n" if $debug;
if ($device->{$path}{size} > $biggest_size)
{
$biggest_size = $device->{$path}{size};
$use_drive = $path;
print __LINE__."; [ Debug ] - use_drive: [".$use_drive."], biggest_size: [".$biggest_size." (".hr_size($biggest_size).")]\n" if $debug;
}
}
if ($use_drive)
{
print "Selected the largest disk: [".$use_drive."], which has a capacity of: [".hr_size($device->{$use_drive}{size})."]\n";
}
}
else
{
# Node and DR are handled the same
my $first_disk_seen = 0;
my $smallest_size = 0;
my $selected_is_platter = 0;
foreach my $path (sort {$a cmp $b} keys %{$device})
{
print __LINE__."; [ Debug ] - first_disk_seen: [".$first_disk_seen."], path: [".$path."], ${path}::rotational: [".$device->{$path}{rotational}."]\n" if $debug;
if (not $first_disk_seen)
{
# Select this one
$first_disk_seen = 1;
$use_drive = $path;
$smallest_size = $device->{$path}{size};
$selected_is_platter = $device->{$path}{rotational};
print __LINE__."; [ Debug ] - first_disk_seen: [".$first_disk_seen."], use_drive: [".$use_drive."], selected_is_platter: [".$selected_is_platter."], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
}
elsif ($device->{$path}{rotational})
{
# This takes priority
print __LINE__."; [ Debug ] - selected_is_platter: [".$selected_is_platter."]\n" if $debug;
if ($selected_is_platter)
{
# Was the previously seen drive bigger?
print __LINE__."; [ Debug ] - ".$path."::size: [".$first_disk_seen." (".hr_size($first_disk_seen).")], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
if ($device->{$path}{size} < $smallest_size)
{
# This is smaller, use it.
$use_drive = $path;
$smallest_size = $device->{$path}{size};
print __LINE__."; [ Debug ] - use_drive: [".$use_drive."], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
}
}
else
{
# The previous drive is an SSD, so use this one regardless
$use_drive = $path;
$smallest_size = $device->{$path}{size};
$selected_is_platter = $device->{$path}{rotational};
print __LINE__."; [ Debug ] - use_drive: [".$use_drive."], selected_is_platter: [".$selected_is_platter."], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
}
}
elsif (not $selected_is_platter)
{
# This is an SSD, but we haven't seen a platter drive yet, so use it if it is
# smaller.
print __LINE__."; [ Debug ] - ".$path."::size: [".$first_disk_seen." (".hr_size($first_disk_seen).")], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
if ($device->{$path}{size} < $smallest_size)
{
# This is smaller, use it.
$use_drive = $path;
$smallest_size = $device->{$path}{size};
print __LINE__."; [ Debug ] - use_drive: [".$use_drive."], smallest_size: [".$smallest_size." (".hr_size($smallest_size).")]\n" if $debug;
}
}
}
# Did we find a drive?
if ($use_drive)
{
if ($selected_is_platter)
{
print "Selected the smallest platter drive: [".$use_drive."], which has a capacity of: [".hr_size($device->{$use_drive}{size})."]\n";
}
else
{
print "Selected the smallest solid-state drive: [".$use_drive."], which has a capacity of: [".hr_size($device->{$use_drive}{size})."] (no platter drives found)\n";
}
}
}
# Did we find a disk to use?
if (not $use_drive)
{
print "[ Error ] - Failed to find any fixed drives (platter or USB, not removable) to install onto. Unable to proceed.\n";
exit(2);
}
# Pick up a bit of a UUID to add to the volume group name.
my $id = time;
if ((-e "/sys/class/dmi/id/product_uuid") && (-r "/sys/class/dmi/id/product_uuid"))
{
# We should be able to read the system UUID. If so, we'll take the starting part of the string for
# the short ID.
my $uuid = "";
my $shell_call = "/sys/class/dmi/id/product_uuid";
print __LINE__."; [ Debug ] - shell_call: [".$shell_call."]\n" if $debug;
open (my $file_handle, "<", $shell_call) or die "Failed to read: [".$shell_call."], the error was: ".$!."\n";
while(<$file_handle>)
{
chomp;
$uuid = $_;
print __LINE__."; [ Debug ] - uuid: [".$uuid."]\n" if $debug;
}
close $file_handle;
if ($uuid =~ /^(\w+)-/)
{
$id = $1;
print __LINE__."; [ Debug ] - id: [".$id."]\n" if $debug;
}
}
### NOTE: RAID 0 is not RAID (literally or in this case). So '0' means 'no raid'
# If I have 2+ drives of the same size as 'use_drive', I will create a RAID array.
my $raid_level = 0;
my $hr_size = $device->{$use_drive}{hr_size};
my $count = @{$drives->{by_hr_size}{$hr_size}};
print __LINE__."; [ Debug ] - Drives of size: [".$hr_size."]: [".$count."].\n" if $debug;
if ($count == 0)
{
$raid_level = 0;
}
elsif ($count == 2)
{
$raid_level = 1;
}
elsif ($count == 4)
{
$raid_level = 10;
}
elsif (($count == 3) or ($count == 5))
{
$raid_level = 5;
}
elsif ($count > 5)
{
$raid_level = 6;
}
my $say_use_drive = $use_drive;
if (not $raid_level)
{
print "Building a standard partition layout for: [".$use_drive."] which is: [".$hr_size."]\n";
}
else
{
print "Building a software RAID level: [".$raid_level."] array using the: [".$count."x] [".$hr_size."] drives;\n";
$say_use_drive = "";
foreach my $path (sort {$a cmp $b} @{$drives->{by_hr_size}{$hr_size}})
{
print "- ".$path."\n";
$say_use_drive .= $path.",";
}
$say_use_drive =~ s/,$//;
}
### NOTE: kickstart sizes are in MiB
# Prepare some variables
my $swap_size = 8192;
my $root_size = 0;
my $vg_name = $type."_".$id;
# If this machine has a small size, we'll cut back the swap and root sizes.
my $per_disk_space = sprintf("%.2f", ($device->{$use_drive}{size} /= (2 ** 20)));
my $available_space = $per_disk_space;
print __LINE__."; [ Debug ] - per_disk_space: [".$per_disk_space." (".hr_size($per_disk_space * (2**20)).")], available_space: [".$available_space." (".hr_size($available_space * (2**20)).")]\n" if $debug;
if ($raid_level == 10)
{
# Total == 2 x single disk
$available_space *= 2;
print __LINE__."; [ Debug ] - available_space: [".$available_space."]\n" if $debug;
}
elsif ($raid_level == 5)
{
# Total == count x Disks - 1
$available_space = ($per_disk_space * $count) - $per_disk_space;
print __LINE__."; [ Debug ] - available_space: [".$available_space."]\n" if $debug;
}
elsif ($raid_level == 6)
{
# Total == count x Disks - 2
$available_space = ($per_disk_space * $count) - ($per_disk_space * 2);
print __LINE__."; [ Debug ] - available_space: [".$available_space."]\n" if $debug;
}
# Now, how much space is available after taking some for BIOSBOOT and /boot ?
$available_space -= 2;
print __LINE__."; [ Debug ] - available_space: [".$available_space." (".hr_size($available_space * (2**20)).")]\n" if $debug;
if ($available_space < 40960)
{
# Not enough space for the standard layout.
$swap_size = 4096;
print __LINE__."; [ Debug ] - swap_size: [".$swap_size."]\n" if $debug;
}
# The left over space is for '/' (we'll shorten this up to 40GiB for nodes and DR hosts next)
$root_size = $available_space - $swap_size;
print __LINE__."; [ Debug ] - root_size: [".$root_size."]\n" if $debug;
print __LINE__."; [ Debug ] - type: [".$type."], root_size: [".$root_size."]\n" if $debug;
if (($type ne "striker") && ($root_size > 40960))
{
$root_size = 40960;
print __LINE__."; [ Debug ] - root_size: [".$root_size."]\n" if $debug;
}
# Round down to an event integer.
$root_size =~ s/\.\d+$//;
print __LINE__."; Assigning: [".hr_size($swap_size * (2**20))." (".$swap_size." MiB)], root_size: [".hr_size($root_size * (2**20))." (".$root_size.") MiB]\n" if $debug;
# Build the partition file.
my $partition_file = "/tmp/plan_partitions.out";
my $partition_body = "zerombr
clearpart --all --drives=".$say_use_drive."
ignoredisk --only-use=".$say_use_drive."
bootloader --location=mbr --driveorder=".$say_use_drive." --boot-drive=".$use_drive;
if (not $raid_level)
{
# Finally, we've got our output.
$partition_body .= "
# Partitions
part biosboot --fstype=biosboot --size=2
part pv.01 --fstype=lvmpv --size=100 --ondisk=".$use_drive." --grow
# LVM Volume groups
volgroup ".$vg_name." --pesize=4096 pv.01
# LVM logical volumes
logvol swap --fstype=swap --size=".$swap_size." --name=lv_swap --vgname=".$vg_name."
logvol / --fstype=xfs --size=100 --grow --maxsize=".$root_size." --name=lv_root --vgname=".$vg_name."
";
}
else
{
$partition_body .= "
# biosboot
";
for (my $i = 0; $i < $count; $i++)
{
$partition_body .= "part biosboot --fstype=biosboot --size=2 --ondisk=".$drives->{by_hr_size}{$hr_size}->[$i]."\n";
}
$partition_body .= "
# LVM PV
";
my $say_raid = "";
for (my $i = 0; $i < $count; $i++)
{
my $disk_number = $i + 1;
$partition_body .= "part raid.1".$disk_number." --size 100 --grow --ondisk=".$drives->{by_hr_size}{$hr_size}->[$i]."\n";
$say_raid .= "raid.1".$disk_number." ";
}
$partition_body .= "raid pv.01 --fstype=xfs --device=pv.01 --level=RAID".$raid_level." ".$say_raid."
# LVM Volume groups
volgroup ".$vg_name." pv.01
# LVM logical volumes
logvol swap --fstype=swap --size=".$swap_size." --name=lv_swap --vgname=".$vg_name."
logvol / --fstype=xfs --size=100 --grow --maxsize=".$root_size." --name=lv_root --vgname=".$vg_name."
";
}
### NOTE: This shouldn't be needed... See: https://bugzilla.redhat.com/show_bug.cgi?id=1654902
# Wipe out the start of each disk so that the install doesn't puke if it sees, for example, an mdadm
# signature on the sole disk being used as an install target.
foreach my $path (split/,/, $say_use_drive)
{
print "[ NOTE ] - Wiping the boot sector of: [".$path."] and configuring it for a GPT label.\n";
my $dd_out = system_call("/bin/dd bs=5120 count=1 if=/dev/zero of=".$path." oflag=dsync");
print __LINE__."; [ Debug ] - dd output:
================================================================================
".$dd_out."
================================================================================\n" if $debug;
my $partprobe_out = system_call("/sbin/partprobe --summary ".$path);
print __LINE__."; [ Debug ] - partprobe summary.
================================================================================
".$partprobe_out."
================================================================================\n" if $debug;
my $partx_out = system_call("/sbin/partx --update --verbose ".$path);
print __LINE__."; [ Debug ] - parted print output showing new layout.
================================================================================
".$partx_out."
================================================================================\n" if $debug;
}
# Flush things out. The article says to blindly sleep 30, but it says to do so to make sure udev, partx and
# others have updated. We're forcing the issue, which should be faster and safer.
system_call("/bin/sync");
system_call("/sbin/udevadm settle");
# Write out the file.
print __LINE__."; [ Debug ] - partition_body:
================================================================================
".$partition_body."
================================================================================\n";
print "Writing out the partition plan to: [".$partition_file."]\n";
# Write it to the temp file that the kickstart's %include will look for.
my $shell_call = $partition_file;
print __LINE__."; [ Debug ] - shell_call: [".$shell_call."]\n" if $debug;
open (my $file_handle, ">", $shell_call) or die "Failed to write: [".$shell_call."], the error was: ".$!."\n";
print $file_handle $partition_body;
close $file_handle;
print "Completed successfully, exiting.\n";
# We're done.
exit(0);
### Functions
# Make the size easier to read for users
sub hr_size
{
my ($size) = @_;
my $hr_size = $size;
if ($size < 1023)
{
# Bytes
$hr_size .= " B";
}
elsif ($size < (2 ** 20))
{
# Kibibyte
$hr_size = sprintf("%.1f", ($size /= (2 ** 10)))." KiB";
}
elsif ($size < (2 ** 30))
{
# Mebibyte
$hr_size = sprintf("%.2f", ($size /= (2 ** 20)))." MiB";
}
elsif ($size < (2 ** 40))
{
# Gibibyte
$hr_size = sprintf("%.2f", ($size /= (2 ** 30)))." GiB";
}
elsif ($size < (2 ** 50))
{
# Tebibyte
$hr_size = sprintf("%.2f", ($size /= (2 ** 40)))." TiB";
}
else
{
# Pebibyte or higher
$hr_size = sprintf("%.3f", ($size /= (2 ** 40)))." PiB";
}
return($hr_size);
}
sub system_call
{
my ($command) = @_;
my $output = "";
open (my $file_handle, $command." 2>&1 |") or die "Failed to call: [".$command."], error was: [".$!."]\n";
while (<$file_handle>)
{
chomp;
my $line = $_;
$line =~ s/\n$//;
$line =~ s/\r$//;
$output .= $line."\n";
}
close $file_handle;
$output =~ s/\n$//s;
return($output);
}