anvil/scripts/plan_partitions
Digimer e8efbab343 * The work on PXE / UEFI support is broken, and will be set aside for the time being. The commit here is working to getting things fixed, but it's taking too much time away from more pressing issues.
* This commit includes two unrelated test files for UI work, cgi-bin/get_anvil_status and cgi-bin/get_anvils.

Signed-off-by: Digimer <digimer@alteeve.ca>
2021-02-16 17:32:43 -05:00

493 lines
17 KiB
Perl
Executable File

#!/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 = "ignoredisk --only-use=".$say_use_drive."
clearpart --none --initlabel";
if (not $raid_level)
{
# Finally, we've got our output.
$partition_body .= "
# Disk partitioning information
part biosboot --fstype=\"biosboot\" --ondisk=".$use_drive." --size=2
part /boot --fstype=\"xfs\" --ondisk=".$use_drive." --size=1024
part /boot/efi --fstype=\"efi\" --ondisk=".$use_drive." --size=600 --fsoptions=\"umask=0077,shortname=winnt\"
part pv.01 --fstype=lvmpv --ondisk=".$use_drive." --size=100 --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\" --ondisk=".$drives->{by_hr_size}{$hr_size}->[$i]." --size=2 \n";
$partition_body .= "part /boot/efi --fstype=\"efi\" --ondisk=".$drives->{by_hr_size}{$hr_size}->[$i]." --size=600 --fsoptions=\"umask=0077,shortname=winnt\" \n";
$partition_body .= "part /boot --fstype=\"xfs\" --ondisk=".$drives->{by_hr_size}{$hr_size}->[$i]." --size=1024 \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);
}