Files
webmin/bsdfdisk-lib.pl
2026-02-04 08:39:11 +01:00

2085 lines
78 KiB
Perl

BEGIN { push( @INC, ".." ); }
use WebminCore;
init_config();
foreign_require( "mount", "mount-lib.pl" );
foreign_require( "fdisk", "fdisk-lib.pl" );
#---------------------------------------------------------------------
# Helper: Cache mount info
# Returns a hash reference of device => mount point and an arrayref
# containing mount entries.
sub get_all_mount_points_cached {
my %mount_info;
my @mount_list = mount::list_mounted();
foreach my $m (@mount_list) {
$mount_info{ $m->[0] } = $m->[1];
}
my $swapinfo = backquote_command("swapinfo -k 2>/dev/null");
foreach my $line ( split( /\n/, $swapinfo ) ) {
if ( $line =~ /^(\/dev\/\S+)\s+\d+\s+\d+\s+\d+/ ) {
$mount_info{$1} = "swap";
}
}
# ZFS, GEOM, glabel, geli remain the same as the original.
if ( has_command("zpool") ) {
my $zpool_out = backquote_command("zpool status 2>/dev/null");
my $current_pool = "";
foreach my $line ( split( /\n/, $zpool_out ) ) {
if ( $line =~ /^\s*pool:\s+(\S+)/ ) {
$current_pool = $1;
}
elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
my $dev = $1;
$mount_info{$dev} = "ZFS pool: $current_pool";
}
}
}
if ( has_command("geom") ) {
# gmirror
my $gmirror_out = backquote_command("gmirror status 2>/dev/null");
my $current_mirror = "";
foreach my $line ( split( /\n/, $gmirror_out ) ) {
if ( $line =~ /^(\S+):/ ) {
$current_mirror = $1;
}
elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
my $dev = $1;
$mount_info{$dev} = "gmirror: $current_mirror";
}
}
# gstripe
my $gstripe_out = backquote_command("gstripe status 2>/dev/null");
my $current_stripe = "";
foreach my $line ( split( /\n/, $gstripe_out ) ) {
if ( $line =~ /^(\S+):/ ) {
$current_stripe = $1;
}
elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
my $dev = $1;
$mount_info{$dev} = "gstripe: $current_stripe";
}
}
# graid
my $graid_out = backquote_command("graid status 2>/dev/null");
my $current_raid = "";
foreach my $line ( split( /\n/, $graid_out ) ) {
if ( $line =~ /^(\S+):/ ) {
$current_raid = $1;
}
elsif ( $line =~ /^\s*(\/dev\/\S+)/ ) {
my $dev = $1;
$mount_info{$dev} = "graid: $current_raid";
}
}
}
if ( has_command("glabel") ) {
my $glabel_out = backquote_command("glabel status 2>/dev/null");
foreach my $line ( split( /\n/, $glabel_out ) ) {
if ( $line =~ /^\s*(\S+)\s+(\S+)\s+(\S+)/ ) {
my $label = $1;
my $dev = $3;
if ( $dev =~ /^\/dev\// ) {
$mount_info{$dev} = "glabel: $label";
}
}
}
}
if ( has_command("geli") ) {
my $geli_out = backquote_command("geli status 2>/dev/null");
foreach my $line ( split( /\n/, $geli_out ) ) {
if ( $line =~ /^(\/dev\/\S+)\s+/ ) {
my $dev = $1;
$mount_info{$dev} = "geli encrypted";
}
}
}
return ( \%mount_info, \@mount_list );
}
#---------------------------------------------------------------------
# Helper: Get file statistics for a device (cached per device)
sub get_dev_stat {
my ($dev) = @_;
if ( -e $dev ) {
my @st = stat($dev);
if (@st) {
my $size = $st[7];
my $blocks = int( $size / 512 );
return ( $size, $blocks );
}
}
return ( undef, undef );
}
#---------------------------------------------------------------------
# is_boot_partition()
# Accepts a partition hash and an optional mount list to avoid
# re-calling mount::list_mounted()
sub is_boot_partition {
my ( $part, $mount_list_ref ) = @_;
return 1
if ( $part->{'type'} eq 'freebsd-boot' or $part->{'type'} eq 'efi' );
return 1 if ( $part->{'active'} );
my @mounts = $mount_list_ref ? @$mount_list_ref : mount::list_mounted();
foreach my $m (@mounts) {
if ( $m->[1] eq '/boot' and $m->[0] eq $part->{'device'} ) {
return 1;
}
}
if ( $part->{'type'} eq 'freebsd-zfs' ) {
my $out = backquote_command("zpool get bootfs 2>/dev/null");
if ( $out =~ /\s+bootfs\s+\S+\/boot\s+/ ) {
my $pool_out = backquote_command("zpool status 2>/dev/null");
if ( $pool_out =~ /\Q$part->{'device'}\E/ ) {
return 1;
}
}
}
return 0;
}
#---------------------------------------------------------------------
# list_disks_partitions()
# Returns a list of all disks, slices and partitions (optimized)
sub list_disks_partitions {
my @results;
my %dev_stat_cache; # cache stat info per /dev device
my @disk_devices;
# Get disk devices from /dev directory
if ( opendir( my $dh, "/dev" ) ) {
my @all_devs = readdir($dh);
closedir($dh);
foreach my $dev (@all_devs) {
if ( $dev =~ /^(ada|ad|da|amrd|nvd|vtbd)(\d+)$/ ) {
push( @disk_devices, $dev );
}
}
}
# Fallback: sysctl
if ( !@disk_devices ) {
my $sysctl_out = backquote_command("sysctl -n kern.disks 2>/dev/null");
if ($sysctl_out) {
chomp($sysctl_out);
@disk_devices = split( /\s+/, $sysctl_out );
}
}
# Fallback: dmesg
if ( !@disk_devices ) {
my $dmesg_out = backquote_command(
"dmesg | grep -E '(ada|ad|da|amrd|nvd|vtbd)[0-9]+:' 2>/dev/null");
while ( $dmesg_out =~ /\b(ada|ad|da|amrd|nvd|vtbd)(\d+):/g ) {
my $disk = "$1$2";
push( @disk_devices, $disk ) if ( -e "/dev/$disk" );
}
}
# Fallback: geom
if ( !@disk_devices ) {
my $geom_out = backquote_command("geom disk list 2>/dev/null");
while ( $geom_out =~ /Name:\s+(\S+)/g ) {
my $disk = $1;
push( @disk_devices, $disk ) if ( -e "/dev/$disk" );
}
}
# Get mount information once for all devices
my ( $mount_info, $mount_list ) = get_all_mount_points_cached();
foreach my $disk (@disk_devices) {
my $disk_device = "/dev/$disk";
my $diskinfo = { 'device' => $disk_device, 'name' => $disk };
# Determine sector size once per disk (4K-aware)
my $sectorsz = get_disk_sectorsize($disk_device) || 512;
$diskinfo->{'sectorsize'} = $sectorsz;
# Cache stat information for the disk device
unless ( exists $dev_stat_cache{$disk_device} ) {
my ( $size, $blocks ) = get_dev_stat($disk_device);
$dev_stat_cache{$disk_device} =
( defined $size ) ? [ $size, $blocks || 0 ] : [ 0, 0 ];
}
my ( $size, $blocks_cached ) = @{ $dev_stat_cache{$disk_device} };
if ( $size > 0 ) {
$diskinfo->{'size'} = $size;
$diskinfo->{'blocks'} = int( $size / $sectorsz );
}
else {
my $diskinfo_out = backquote_command(
"diskinfo " . quote_path($disk) . " 2>/dev/null" );
if ( $diskinfo_out =~ /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(.*)/ ) {
$diskinfo->{'size'} = $1;
$diskinfo->{'blocks'} = int( $1 / $sectorsz );
$diskinfo->{'cylinders'} = $2;
$diskinfo->{'heads'} = $3;
$diskinfo->{'sectors'} = $4;
if ( defined $5 && $5 ne '' && $5 !~ /^\d+$/ ) {
$diskinfo->{'model'} = $5;
}
}
if ( !$diskinfo->{'size'} ) {
my $diskinfo_v_out = backquote_command(
"diskinfo -v " . quote_path($disk) . " 2>/dev/null" );
if ( $diskinfo_v_out =~ /sectorsize:\s*(\d+)/i ) {
$sectorsz = $1;
$diskinfo->{'sectorsize'} = $sectorsz;
}
if ( $diskinfo_v_out =~ /mediasize in bytes:\s+(\d+)/i ) {
$diskinfo->{'size'} = $1;
$diskinfo->{'blocks'} = int( $1 / $sectorsz );
}
if ( $diskinfo_v_out =~ /descr:\s+(.*)/ ) {
$diskinfo->{'model'} = $1;
}
}
if ( !$diskinfo->{'model'} ) {
my $cam_id_out =
backquote_command( "camcontrol identify "
. quote_path($disk)
. " 2>/dev/null" );
if ( $cam_id_out =~ /model\s+(.*)/i ) {
my $m = $1;
$m =~ s/^\s+|\s+$//g;
$diskinfo->{'model'} = $m;
}
}
if ( !$diskinfo->{'model'} ) {
my $inq_out =
backquote_command( "camcontrol inquiry "
. quote_path($disk)
. " 2>/dev/null" );
if ( $inq_out =~ /<([^>]+)>/ ) {
$diskinfo->{'model'} = $1;
}
else {
my ($vendor) =
( $inq_out =~ /Vendor:\s*(\S.*?)(?:\s{2,}|$)/i );
my ($product) =
( $inq_out =~ /Product:\s*(\S.*?)(?:\s{2,}|$)/i );
if ( $vendor || $product ) {
$diskinfo->{'model'} = join( ' ',
grep { defined && length } ( $vendor, $product ) );
}
}
}
if ( !$diskinfo->{'model'} ) {
my $geom = get_detailed_disk_info($disk_device);
if ( $geom && $geom->{'descr'} ) {
$diskinfo->{'model'} = $geom->{'descr'};
}
elsif ( $geom && $geom->{'ident'} ) {
$diskinfo->{'model'} = $geom->{'ident'};
}
}
# If size still not known, try GEOM mediasize
if ( !$diskinfo->{'size'} ) {
my $geom2 = get_detailed_disk_info($disk_device);
if ( $geom2 && $geom2->{'mediasize_bytes'} ) {
$diskinfo->{'size'} = $geom2->{'mediasize_bytes'};
$diskinfo->{'blocks'} = int( $diskinfo->{'size'} /
( $diskinfo->{'sectorsize'} || 512 ) );
}
}
}
# Determine disk type
if ( $disk =~ /^ada/ or $disk =~ /^ad/ ) {
$diskinfo->{'type'} = 'ide';
}
elsif ( $disk =~ /^da/ ) {
$diskinfo->{'type'} = 'scsi';
}
elsif ( $disk =~ /^amrd/ ) {
$diskinfo->{'type'} = 'memdisk';
}
elsif ( $disk =~ /^nvd/ ) {
$diskinfo->{'type'} = 'nvme';
}
elsif ( $disk =~ /^vtbd/ ) {
$diskinfo->{'type'} = 'virtio';
}
# Process slices and partitions
$diskinfo->{'slices'} = [];
if ( has_command("gpart") ) {
my $gpart_out = backquote_command(
"gpart show " . quote_path($disk) . " 2>/dev/null" );
my @lines = split( /\n/, $gpart_out );
my $in_disk = 0;
my $disk_scheme = undef; # GPT, MBR, etc.
foreach my $line (@lines) {
if ( $line =~ /=>/ ) {
$in_disk = 1;
# Try to extract scheme from header line
if ( $line =~ /=>.*?\b$disk\b\s+(\S+)/ ) {
$disk_scheme = $1;
}
next;
}
if ( $in_disk and $line =~ /^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ )
{
my ( $start, $num_blocks, $name_or_idx, $raw_type ) =
( $1, $2, $3, $4 );
next if ( $name_or_idx eq '-' or $raw_type eq 'free' );
my $slice_type =
( $raw_type eq '-' ) ? "freebsd" : $raw_type;
# Determine slice index and device name
my ( $slice_index, $slice_devname );
if ( $name_or_idx =~ /^$disk(?:p|s)(\d+)$/ ) {
$slice_index = $1;
$slice_devname = $name_or_idx;
}
elsif ( $name_or_idx =~ /^\d+$/ ) {
$slice_index = $name_or_idx;
my $sep =
( defined $disk_scheme && $disk_scheme =~ /GPT/i )
? 'p'
: 's';
$slice_devname = $disk . $sep . $slice_index;
}
else {
# Fallback: use as provided
$slice_devname = $name_or_idx;
# Try to extract index from suffix if possible
($slice_index) = ( $slice_devname =~ /(?:p|s)(\d+)$/ );
$slice_index ||= $name_or_idx;
}
my $slice_device = "/dev/$slice_devname";
my $slice = {
'number' => $slice_index,
'startblock' => $start,
'blocks' => $num_blocks,
'size' => $num_blocks * $sectorsz,
'type' => $slice_type,
'device' => $slice_device,
'parts' => []
};
$slice->{'used'} = $mount_info{$slice_device};
# Get partitions for this slice once, using the correct provider name
my $gpart_slice_out =
backquote_command( "gpart show "
. quote_path($slice_devname)
. " 2>/dev/null" );
my @slice_lines = split( /\n/, $gpart_slice_out );
my $in_slice = 0;
my $slice_scheme;
foreach my $slice_line (@slice_lines) {
if ( $slice_line =~ /=>.*?\s+$slice_devname\s+(\S+)/ ) {
$in_slice = 1;
$slice_scheme = $1; # e.g., BSD, GPT
next;
}
if ( $in_slice
and $slice_line =~
/^\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)/ )
{
my (
$p_start, $p_blocks,
$part_idx_or_name, $raw_ptype
) = ( $1, $2, $3, $4 );
next
if ( $part_idx_or_name eq '-'
or $raw_ptype eq 'free' );
my $part_type =
( $raw_ptype eq '-' and $part_idx_or_name ne '-' )
? "freebsd-ufs"
: $raw_ptype;
# For BSD disklabel, third column is index (1-based), convert to letter
my ( $part_letter, $part_device );
if ( $slice_scheme
&& $slice_scheme =~ /BSD/i
&& $part_idx_or_name =~ /^\d+$/ )
{
my $idx = int($part_idx_or_name);
$part_letter = chr( ord('a') + $idx - 1 )
; # 1 -> 'a', 2 -> 'b', etc.
$part_device = $slice_device . $part_letter;
}
else {
# For other schemes or if already a name
$part_device = "/dev/$part_idx_or_name";
$part_letter = substr( $part_idx_or_name, -1 );
}
my $part = {
'letter' => $part_letter,
'startblock' => $p_start,
'blocks' => $p_blocks,
'size' => $p_blocks * $sectorsz,
'type' => $part_type,
'device' => $part_device,
};
$part->{'used'} = $mount_info{$part_device};
push( @{ $slice->{'parts'} }, $part );
}
}
push( @{ $diskinfo->{'slices'} }, $slice );
}
}
}
else {
# If no slices found with gpart, use fdisk if available
# (similar caching ideas apply)
if ( has_command("fdisk") ) {
my $fdisk_out = backquote_command(
"fdisk " . quote_path("/dev/$disk") . " 2>/dev/null" );
foreach my $line ( split( /\n/, $fdisk_out ) ) {
if ( $line =~ /^\s*(\d+):\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)/ )
{
my $slice_device = "/dev/${disk}s$1";
my $slice = {
'number' => $1,
'startblock' => $2,
'blocks' => ( $4 - $2 + 1 ),
'size' => ( $4 - $2 + 1 ) * $sectorsz,
'type' => $5,
'device' => $slice_device,
'parts' => []
};
$slice->{'used'} = $mount_info{$slice_device};
my $disklabel_out =
backquote_command( "disklabel -r "
. quote_path($slice_device)
. " 2>/dev/null" );
foreach my $label_line ( split( /\n/, $disklabel_out ) )
{
if ( $label_line =~
/^(\s*)([a-h]):\s+(\d+)\s+(\d+)\s+(\S+)/ )
{
my $part_device = "${slice_device}$2";
my $part = {
'letter' => $2,
'startblock' => $3,
'blocks' => $4,
'size' => $4 * $sectorsz,
'type' => $5,
'device' => $part_device
};
$part->{'used'} = $mount_info{$part_device};
push( @{ $slice->{'parts'} }, $part );
}
}
push( @{ $diskinfo->{'slices'} }, $slice );
}
}
}
}
# If size was not determined, estimate from slices if available
if ( !$diskinfo->{'size'} and @{ $diskinfo->{'slices'} } ) {
my $total_size = 0;
$total_size += $_->{'size'} for @{ $diskinfo->{'slices'} };
if ( $total_size > 0 ) {
$diskinfo->{'size'} = $total_size;
$diskinfo->{'blocks'} =
int( $total_size / ( $diskinfo->{'sectorsize'} || 512 ) );
}
}
# Finally, add this disk (dummy size set if necessary)
if ( $diskinfo->{'size'} or -e $disk_device ) {
$diskinfo->{'size'} = $diskinfo->{'size'} || 0;
$diskinfo->{'blocks'} = $diskinfo->{'blocks'} || 0;
push( @results, $diskinfo );
}
}
return @results;
}
# check_fdisk() unchanged
sub check_fdisk {
if ( !has_command("fdisk") and !has_command("gpart") ) {
return text( 'index_efdisk', "<tt>fdisk</tt>", "<tt>gpart</tt>" );
}
return undef;
}
# is_using_gpart()
sub is_using_gpart {
return has_command("gpart") ? 1 : 0;
}
# disk_name(device) extracts name from /dev/device
sub disk_name {
my ($device) = @_;
$device =~ s/^\/dev\///;
return $device;
}
# slice_name(slice)
sub slice_name {
my ($slice) = @_;
if ( $slice->{'device'} =~ /\/dev\/(\S+)/ ) {
return $1;
}
return $slice->{'number'};
}
# slice_number(slice)
sub slice_number {
my ($slice) = @_;
if ( $slice->{'device'} =~ /\/dev\/\S+s(\d+)/ ) {
return $1;
}
if ( $slice->{'device'} =~ /\/dev\/\S+p(\d+)/ ) {
return $1;
}
return $slice->{'number'};
}
sub _safe_uint {
my ($v) = @_;
return undef unless defined $v;
return undef unless $v =~ /^\d+$/;
return int($v);
}
sub _safe_letter {
my ($v) = @_;
return undef unless defined $v;
return undef unless $v =~ /^[a-z]$/i;
return lc($v);
}
#---------------------------------------------------------------------
# Filesystem command generation and slice/partition modification functions
sub create_slice {
my ( $disk, $slice ) = @_;
my $cmd;
if ( is_using_gpart() ) {
# Ensure a partitioning scheme exists (default to MBR for non-GPT,
# GPT if new) before adding
my $base = disk_name( $disk->{'device'} );
my $ds = get_disk_structure($base);
my $scheme =
'MBR'; # default for existing disks or when type suggests MBR
if ( !$ds || !$ds->{'scheme'} ) {
# No scheme exists - decide based on partition type
if ( $slice->{'type'} =~ /^(freebsd|fat32|ntfs|linux)$/i ) {
$scheme = 'MBR';
}
else {
$scheme = 'GPT';
}
my $init = "gpart create -s $scheme " . quote_path($base);
my $init_out = backquote_command("$init 2>&1");
if ( $? != 0 && $init_out !~ /File exists|already exists/i ) {
return $init_out;
}
# Refresh disk structure after creation
$ds = get_disk_structure($base);
}
else {
$scheme = $ds->{'scheme'};
}
# Defense-in-depth: validate type against allowed list for scheme
my %allowed =
map { $_->[0] => 1 } list_partition_types($scheme);
return $text{'nslice_etype'}
unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );
$cmd = "gpart add -t " . $slice->{'type'};
if ( defined $slice->{'startblock'}
&& $slice->{'startblock'} =~ /^\d+$/ )
{
$cmd .= " -b " . int( $slice->{'startblock'} );
}
if ( defined $slice->{'blocks'} && $slice->{'blocks'} =~ /^\d+$/ ) {
$cmd .= " -s " . int( $slice->{'blocks'} );
}
$cmd .= " " . quote_path($base);
my $out = backquote_command("$cmd 2>&1");
if ($?) {
return $out;
}
# After successful creation, populate the device field for the slice
# Determine the separator based on scheme
my $sep = ( $scheme =~ /GPT/i ) ? 'p' : 's';
my $slice_num = $slice->{'number'};
$slice->{'device'} = "/dev/${base}${sep}${slice_num}";
return undef;
}
else {
my %allowed = map { $_ => 1 } fdisk::list_tags();
return $text{'nslice_etype'}
unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );
$cmd = "fdisk -a";
my $sn = _safe_uint( $slice->{'number'} );
my $sb = _safe_uint( $slice->{'startblock'} );
my $bl = _safe_uint( $slice->{'blocks'} );
$cmd .= " -s $sn" if defined $sn;
$cmd .= " -b $sb" if defined $sb;
$cmd .= " -s $bl" if defined $bl;
$cmd .= " -t $slice->{'type'} " . quote_path( $disk->{'device'} );
my $out = backquote_command("$cmd 2>&1");
if ($?) {
return $out;
}
# Populate device field
my $base = disk_name( $disk->{'device'} );
$slice->{'device'} = "/dev/${base}s" . $slice->{'number'};
return undef;
}
}
sub delete_slice {
my ( $disk, $slice ) = @_;
if ( is_boot_partition($slice) ) { return $text{'slice_eboot'}; }
foreach my $p ( @{ $slice->{'parts'} } ) {
if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; }
}
my $cmd;
if ( is_using_gpart() ) {
my $idx = _safe_uint( slice_number($slice) );
return $text{'slice_egone'} unless defined $idx;
$cmd =
"gpart delete -i "
. $idx . " "
. quote_path( disk_name( $disk->{'device'} ) );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
else {
my $sn = _safe_uint( $slice->{'number'} );
return $text{'slice_egone'} unless defined $sn;
$cmd = "fdisk -d " . $sn . " " . quote_path( $disk->{'device'} );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
}
sub delete_partition {
my ( $disk, $slice, $part ) = @_;
if ( is_boot_partition($part) ) { return $text{'part_eboot'}; }
my $cmd;
if ( is_using_gpart() ) {
# BSD disklabel uses 1-based indexing: 'a' = 1, 'b' = 2, etc.
my $pl = _safe_letter( $part->{'letter'} );
return $text{'part_egone'} unless defined $pl;
my $idx = ( ord($pl) - ord('a') ) + 1;
$cmd = "gpart delete -i $idx " . quote_path( slice_name($slice) );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
else {
my $pl = _safe_letter( $part->{'letter'} );
return $text{'part_egone'} unless defined $pl;
$cmd =
"disklabel -r -w -d " . $pl . " " . quote_path( $slice->{'device'} );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
}
sub modify_slice {
my ( $disk, $oldslice, $slice, $part ) = @_;
if ( is_boot_partition($part) ) { return $text{'part_eboot'}; }
foreach my $p ( @{ $slice->{'parts'} } ) {
if ( is_boot_partition($p) ) { return $text{'slice_eboot'}; }
}
my $cmd;
if ( is_using_gpart() ) {
my $base = disk_name( $disk->{'device'} );
my $ds = get_disk_structure($base);
my $scheme = ( $ds && $ds->{'scheme'} ) ? $ds->{'scheme'} : 'GPT';
my %allowed =
map { $_->[0] => 1 } list_partition_types($scheme);
return $text{'nslice_etype'}
unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );
my $idx = _safe_uint( slice_number($slice) );
return $text{'slice_egone'} unless defined $idx;
$cmd =
"gpart modify -i "
. $idx . " -t "
. $slice->{'type'} . " "
. quote_path($base);
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
else {
my $sn = _safe_uint( $slice->{'number'} );
return $text{'slice_egone'} unless defined $sn;
my %allowed = map { $_ => 1 } fdisk::list_tags();
return $text{'nslice_etype'}
unless ( defined $slice->{'type'} && $allowed{ $slice->{'type'} } );
$cmd =
"fdisk -a -s "
. $sn . " -t "
. $slice->{'type'} . " "
. quote_path( $disk->{'device'} );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
}
sub save_partition {
my ( $disk, $slice, $part ) = @_;
my $cmd;
if ( is_using_gpart() ) {
my $provider = slice_name($slice);
# Detect if this provider is a BSD label (sub-partitions) or GPT/MBR
my $show =
backquote_command( "gpart show " . quote_path($provider) . " 2>&1" );
if ( $show =~ /\bBSD\b/ ) {
# Inner BSD label: index is 1-based a->1, b->2, etc. Only
# FreeBSD partition types are valid here.
my $pl = _safe_letter( $part->{'letter'} );
return $text{'part_egone'} unless defined $pl;
my $idx = ( ord($pl) - ord('a') ) + 1;
my %allowed =
map { $_->[0] => 1 } list_partition_types('BSD');
return $text{'part_etype'}
unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );
$cmd =
"gpart modify -i $idx -t "
. $part->{'type'} . " "
. quote_path($provider);
}
else {
# Not a BSD label; modifying a top-level partition by letter is
# invalid. Return an error with guidance.
return
"Invalid operation: attempting to modify non-BSD sub-partition "
. "by letter. Use slice editing for top-level partitions.";
}
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
else {
my $pl = _safe_letter( $part->{'letter'} );
return $text{'part_egone'} unless defined $pl;
my %allowed = map { $_->[0] => 1 } list_partition_types('BSD');
return $text{'part_etype'}
unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );
$cmd =
"disklabel -r -w -p "
. $pl . " -t "
. $part->{'type'} . " "
. quote_path( $slice->{'device'} );
my $out = backquote_command("$cmd 2>&1");
return ($?) ? $out : undef;
}
}
# Create a new BSD partition inside an MBR slice (gpart BSD label)
sub create_partition {
my ( $disk, $slice, $part ) = @_;
if ( !is_using_gpart() ) {
# Legacy path would use disklabel; not implemented here
return "Legacy disklabel creation not supported";
}
my $prov = slice_name($slice);
# Ensure BSD label exists on the slice
my $show = backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
if ( $show !~ /\bBSD\b/ ) {
my $init_err = initialize_slice( $disk, $slice );
return $init_err if ($init_err);
# Refresh the show output after initialization
$show =
backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
}
# Compute 1-based index
my $pl = _safe_letter( $part->{'letter'} );
return $text{'part_egone'} unless defined $pl;
my $idx = ( ord($pl) - ord('a') ) + 1;
my %allowed = map { $_->[0] => 1 } list_partition_types('BSD');
return $text{'part_etype'}
unless ( defined $part->{'type'} && $allowed{ $part->{'type'} } );
# For BSD disklabel, start blocks are ALWAYS slice-relative
# BSD partitions use 0-based addressing within the slice
my $start_rel = _safe_uint( $part->{'startblock'} );
my $blocks = _safe_uint( $part->{'blocks'} );
my $cmd = "gpart add -i $idx -t $part->{'type'}";
$cmd .= " -b $start_rel" if ( defined $start_rel && $start_rel > 0 );
$cmd .= " -s $blocks" if ( defined $blocks && $blocks > 0 );
$cmd .= " " . quote_path($prov);
my $out = backquote_command("$cmd 2>&1");
if ($?) {
return $out;
}
# Populate the device field for the partition
$part->{'device'} = $slice->{'device'} . $part->{'letter'};
return undef;
}
sub get_create_filesystem_command {
my ( $disk, $slice, $part, $options ) = @_;
my $device = $part ? $part->{'device'} : $slice->{'device'};
my @cmd = ("newfs");
if ( defined $options->{'free'} && $options->{'free'} =~ /^\d+$/ ) {
push( @cmd, "-m", $options->{'free'} );
}
if ( defined $options->{'label'} && length $options->{'label'} ) {
push( @cmd, "-L", quote_path( $options->{'label'} ) );
}
push( @cmd, "-t" ) if ( $options->{'trim'} );
push( @cmd, quote_path($device) );
return join( " ", @cmd );
}
# Helper to set a GPT or BSD partition label after filesystem creation
sub set_partition_label {
my (%args) = @_;
my $disk = $args{'disk'};
my $slice = $args{'slice'};
my $part = $args{'part'}; # optional
my $label = $args{'label'};
return if ( !defined $label || $label eq '' );
my $base = $disk->{'device'};
$base =~ s{^/dev/}{};
my $ds = get_disk_structure($base);
# GPT: label at disk level via gpart modify
if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
my $idx = $part ? undef : $slice->{'number'}; # slice is a GPT partition
if ($idx) {
my $cmd =
"gpart modify -i $idx -l "
. quote_path($label) . " "
. quote_path($base);
my $out = backquote_command("$cmd 2>&1");
return ( $? ? $out : undef );
}
return undef;
}
# MBR: use glabel for slice-level or partition-level labels
my $device = $part ? $part->{'device'} : $slice->{'device'};
if ( $device && has_command('glabel') ) {
# Remove existing glabel if present, then add new one
my $existing = backquote_command(
"glabel status 2>/dev/null | grep " . quote_path($device) );
if ( $existing =~ /^(\S+)\s+/ ) {
my $old_label = $1;
my $destroy_out = backquote_command(
"glabel destroy " . quote_path($old_label) . " 2>&1" );
}
my $cmd =
"glabel label " . quote_path($label) . " " . quote_path($device);
my $out = backquote_command("$cmd 2>&1");
return ( $? ? $out : undef );
}
return undef;
}
sub remove_partition_label {
my (%args) = @_;
my $disk = $args{'disk'};
my $slice = $args{'slice'};
my $part = $args{'part'};
my $base = $disk->{'device'};
$base =~ s{^/dev/}{};
my $ds = get_disk_structure($base);
# GPT: remove label via gpart modify -l ""
if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
my $idx = $part ? undef : $slice->{'number'};
if ($idx) {
my $cmd = "gpart modify -i $idx -l \"\" " . quote_path($base);
my $out = backquote_command("$cmd 2>&1");
return ( $? ? $out : undef );
}
return undef;
}
# MBR: remove glabel
my $device = $part ? $part->{'device'} : $slice->{'device'};
if ( $device && has_command('glabel') ) {
my $existing = backquote_command(
"glabel status 2>/dev/null | grep " . quote_path($device) );
if ( $existing =~ /^(\S+)\s+/ ) {
my $label = $1;
my $cmd = "glabel destroy $label";
my $out = backquote_command("$cmd 2>&1");
return ( $? ? $out : undef );
}
}
return undef;
}
# Get the current label for a slice/partition device (GPT label or glabel)
sub get_device_label_name {
my (%args) = @_;
my $disk = $args{'disk'};
my $slice = $args{'slice'};
my $device = $args{'device'} || ( $slice ? $slice->{'device'} : undef );
return undef unless $device;
my $label;
# Prefer GPT labels when applicable
my $base = $disk ? $disk->{'device'} : base_disk_device($device);
if ($base) {
$base =~ s{^/dev/}{};
my $ds = $args{'disk_structure'} || get_disk_structure($base);
if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
my $idx = $slice ? slice_number($slice) : undef;
if ( $idx && $ds->{'partitions'} && $ds->{'partitions'}->{$idx} ) {
my $pl = $ds->{'partitions'}->{$idx}->{'label'};
$label = $pl if ( $pl && $pl ne '(null)' );
}
if ( !$label && $idx && $ds->{'entries'} ) {
foreach my $e ( @{ $ds->{'entries'} } ) {
next unless ( $e->{'type'} && $e->{'type'} eq 'partition' );
next
unless ( defined $e->{'index'} && $e->{'index'} == $idx );
if ( $e->{'label'} && $e->{'label'} ne '(null)' ) {
$label = $e->{'label'};
last;
}
}
}
}
}
# Fallback: GEOM label (glabel)
if ( !$label && has_command('glabel') ) {
my $glabel_out = backquote_command("glabel status 2>/dev/null");
foreach my $line ( split( /\n/, $glabel_out ) ) {
if ( $line =~ /^\s*(\S+)\s+\S+\s+(\S+)/ ) {
my ( $lab, $prov ) = ( $1, $2 );
$prov = "/dev/$prov" if ( $prov !~ m{^/dev/} );
if ( $prov eq $device ) {
$label = $lab;
last;
}
}
}
}
return $label;
}
sub preferred_device_path {
my ($device) = @_;
return $device unless $device;
# Check for GPT label first
if ( -e "/dev/gpt" ) {
my $gpt_label =
backquote_command( "gpart list 2>/dev/null | grep -A 10 "
. quote_path($device)
. " | grep 'label:' | head -1" );
if ( $gpt_label =~ /label:\s*(\S+)/ && $1 ne '(null)' ) {
my $label_path = "/dev/gpt/$1";
return $label_path if ( -e $label_path );
}
}
# Check for glabel label
if ( has_command('glabel') ) {
my $glabel_out = backquote_command("glabel status 2>/dev/null");
foreach my $line ( split( /\n/, $glabel_out ) ) {
if ( $line =~ /^(\S+)\s+\S+\s+(.+)$/ ) {
my ( $label, $provider ) = ( $1, $2 );
$provider =~ s/^\s+|\s+$//g;
if ( $provider eq $device || "/dev/$provider" eq $device ) {
my $label_path = "/dev/label/$label";
return $label_path if ( -e $label_path );
}
}
}
}
# Check for UFS label
my $ufs_label =
backquote_command( "tunefs -p "
. quote_path($device)
. " 2>/dev/null | grep 'volume label'" );
if ( $ufs_label =~ /volume label.*\[([^\]]+)\]/ && $1 ne '' ) {
my $label_path = "/dev/ufs/$1";
return $label_path if ( -e $label_path );
}
# Default: return original device
return $device;
}
sub detect_filesystem_type {
my ( $device, $hint ) = @_;
my $t;
if ( has_command('fstyp') ) {
$t =
backquote_command( "fstyp " . quote_path($device) . " 2>/dev/null" );
$t =~ s/[\r\n]+$//;
}
$t ||= $hint || '';
$t = lc($t);
# Normalize common variants
if ( $t =~ /ufs/ ) { return 'ufs'; }
if ( $t =~ /msdos|fat/ ) { return 'msdosfs'; }
if ( $t =~ /ext[234]|ext2fs/ ) { return 'ext2fs'; }
if ( $t =~ /zfs/ ) { return 'zfs'; }
if ( $t =~ /swap/ ) { return 'swap'; }
if ( $t =~ /^(ufs|ffs)$/ ) { return 'ufs'; }
if ( $t =~ /^(msdos|msdosfs|fat|fat32)$/ ) { return 'msdosfs'; }
if ( $t =~ /^(ext2|ext2fs)$/ ) { return 'ext2fs'; }
if ( $t =~ /^zfs$/ ) { return 'zfs'; }
if ( $t =~ /^swap/ ) { return 'swap'; }
return $t || undef;
}
# Apply recommended ACL inherit flags for NFSv4 datasets (best-effort)
sub acl_inherit_flags_cmd {
my ($dataset) = @_;
return undef if ( !$dataset );
my $ds = quote_path($dataset);
my $cmd =
'acltype=$(zfs get -H -o value acltype '
. $ds
. ' 2>/dev/null); '
. 'mp=$(zfs get -H -o value mountpoint '
. $ds
. ' 2>/dev/null); '
. 'if [ "$acltype" = "nfsv4" ] && [ -n "$mp" ] && '
. '[ "$mp" != "-" ] && [ "$mp" != "none" ]; then ';
if ( $^O eq 'freebsd' ) {
$cmd .=
'if command -v getfacl >/dev/null 2>&1 && '
. 'command -v setfacl >/dev/null 2>&1; then '
. 'po=$(getfacl "$mp" 2>/dev/null | awk -F: '
. '"/^[[:space:]]*owner@/{print \\$2; exit}"); '
. 'pg=$(getfacl "$mp" 2>/dev/null | awk -F: '
. '"/^[[:space:]]*group@/{print \\$2; exit}"); '
. 'pe=$(getfacl "$mp" 2>/dev/null | awk -F: '
. '"/^[[:space:]]*everyone@/{print \\$2; exit}"); '
. 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then '
. 'setfacl -m "owner@:$po:fd-----:allow" '
. '-m "group@:$pg:fd-----:allow" '
. '-m "everyone@:$pe:fd-----:allow" "$mp"; '
. 'fi; '
. 'fi; ';
}
else {
$cmd .=
'if command -v nfs4_getfacl >/dev/null 2>&1 && '
. 'command -v nfs4_setfacl >/dev/null 2>&1; then '
. 'po=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
. '"/OWNER@/{print \\$4; exit}"); '
. 'pg=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
. '"/GROUP@/{print \\$4; exit}"); '
. 'pe=$(nfs4_getfacl "$mp" 2>/dev/null | awk -F: '
. '"/EVERYONE@/{print \\$4; exit}"); '
. 'if [ -n "$po" ] && [ -n "$pg" ] && [ -n "$pe" ]; then '
. 'nfs4_setfacl -a "A::OWNER@:$po:fd-----:allow" '
. '-a "A::GROUP@:$pg:fd-----:allow" '
. '-a "A::EVERYONE@:$pe:fd-----:allow" "$mp"; '
. 'fi; '
. 'fi; ';
}
$cmd .= "fi";
return $cmd;
}
sub get_zfs_device_info {
my ($object) = @_;
return undef unless ( $object && $object->{'device'} );
my ( $pools, $zfs_devices ) = build_zfs_devices_cache();
my $device = $object->{'device'};
my @ids = ($device);
( my $short = $device ) =~ s{^/dev/}{};
push( @ids, $short, lc($device), lc($short) );
# If this is a top-level partition, include GPT label aliases if present
if ( $device =~ m{^/dev/([a-z]+[0-9]+)([ps])(\d+)$}i ) {
my ( $base, $sep, $num ) = ( $1, $2, $3 );
my $ds = get_disk_structure($base);
my $part_label;
if ( $ds && $ds->{'partitions'} && $ds->{'partitions'}->{$num} ) {
my $pl = $ds->{'partitions'}->{$num}->{'label'};
$part_label = $pl if ( $pl && $pl ne '(null)' );
}
if ( !$part_label && $ds && $ds->{'entries'} ) {
foreach my $e ( @{ $ds->{'entries'} } ) {
next unless ( $e->{'type'} && $e->{'type'} eq 'partition' );
next unless ( defined $e->{'index'} && $e->{'index'} == $num );
if ( $e->{'label'} && $e->{'label'} ne '(null)' ) {
$part_label = $e->{'label'};
last;
}
}
}
my $part_name = $base . $sep . $num;
my $scheme = ( $sep eq 'p' ) ? 'GPT' : '';
push(
@ids,
_possible_partition_ids(
$base, $scheme, $num, $part_name, $part_label
)
);
}
return _find_in_zfs( $zfs_devices, @ids );
}
sub is_zfs_device {
my ($object) = @_;
return 0 unless ( $object && $object->{'device'} );
# Trust explicit type hints first
if ( defined $object->{'type'} && $object->{'type'} =~ /zfs/i ) {
return 1;
}
return get_zfs_device_info($object) ? 1 : 0;
}
sub get_check_filesystem_command {
my ( $disk, $slice, $part ) = @_;
my $device = $part ? $part->{'device'} : $slice->{'device'};
my $hint = $part ? $part->{'type'} : $slice->{'type'};
my $fstype = detect_filesystem_type( $device, $hint );
my $qdev = quote_path($device);
# Map to specific fsck tools when available; else use fsck -t
if ( $fstype && $fstype eq 'ufs' ) {
return has_command('fsck_ufs')
? "fsck_ufs -y $qdev"
: "fsck -t ufs -y $qdev";
}
if ( $fstype && $fstype eq 'msdosfs' ) {
return has_command('fsck_msdosfs')
? "fsck_msdosfs -y $qdev"
: "fsck -t msdosfs -y $qdev";
}
if ( $fstype && $fstype eq 'ext2fs' ) {
return has_command('fsck_ext2fs')
? "fsck_ext2fs -y $qdev"
: "fsck -t ext2fs -y $qdev";
}
if ( $fstype && $fstype eq 'zfs' ) {
return
"zpool status 2>&1"
; # caller should avoid fsck for ZFS, but safe fallback
}
if ( $fstype && $fstype eq 'swap' ) {
return "echo 'swap device - fsck not applicable'";
}
# Generic fallback
return "fsck -y $qdev";
}
sub show_filesystem_buttons {
my ( $hiddens, $st, $object, $return_url ) = @_;
if ( $return_url && $return_url !~ m{^/} ) {
$return_url = "/$module_name/$return_url";
}
# Use preferred device path (label-based if available)
my $preferred_dev = preferred_device_path( $object->{'device'} );
print ui_buttons_row( "newfs_form.cgi", $text{'part_newfs'},
$text{'part_newfsdesc'}, $hiddens );
# Offer fsck only when a filesystem is detectable and supported
my $is_swap = ( @$st && $st->[1] eq 'swap' )
|| ( $object->{'type'} && $object->{'type'} =~ /freebsd-swap|^82$/i );
my $is_zfs = is_zfs_device($object);
my $fsck_type;
if ( has_command('fstyp') ) {
$fsck_type = detect_filesystem_type( $object->{'device'}, undef );
if ( !$fsck_type ) {
$fsck_type =
detect_filesystem_type( $object->{'device'}, $object->{'type'} );
}
}
else {
$fsck_type =
detect_filesystem_type( $object->{'device'}, $object->{'type'} );
}
if ( $fsck_type
&& $fsck_type ne 'swap'
&& $fsck_type ne 'zfs'
&& !$is_swap
&& !$is_zfs )
{
print ui_buttons_row( "fsck.cgi", $text{'part_fsck'},
$text{'part_fsckdesc'}, $hiddens );
}
if ( !@$st ) {
if ( $object->{'type'} eq 'swap'
or $object->{'type'} eq '82'
or $object->{'type'} eq 'freebsd-swap' )
{
my $mount_h = ui_hidden( "newdev", $preferred_dev )
. ui_hidden( "type", "swap" );
$mount_h .= ui_hidden( "return", $return_url ) if ($return_url);
print ui_buttons_row( "../mount/edit_mount.cgi",
$text{'part_newmount2'}, $text{'part_mountmsg2'}, $mount_h );
}
else {
my $fstype =
detect_filesystem_type( $object->{'device'}, $object->{'type'} );
my $can_mount =
( $fstype && $fstype ne 'zfs' && $fstype ne 'swap' ) ? 1 : 0;
if ($can_mount) {
my $mount_h = ui_hidden( "newdev", $preferred_dev )
. ui_hidden( "type", $fstype );
$mount_h .= ui_hidden( "return", $return_url ) if ($return_url);
print ui_buttons_row( "../mount/edit_mount.cgi",
$text{'part_newmount'}, $text{'part_mountmsg'}, $mount_h );
}
}
}
}
#---------------------------------------------------------------------
# ZFS and GEOM related functions are largely unchanged.
sub get_all_zfs_info {
# Wrapper built from the structured ZFS devices cache
my ( $pools, $devices ) = build_zfs_devices_cache();
my %zfs_info;
foreach my $id ( keys %$devices ) {
next unless $id =~ /^\/dev\//; # focus on canonical /dev/* keys
my $dev = $devices->{$id};
my $suffix =
( $dev->{'vdev_type'} && $dev->{'vdev_type'} eq 'log' )
? '(log)'
: '(data)';
$zfs_info{$id} = $dev->{'pool'} . ' ' . $suffix;
}
return \%zfs_info;
}
sub get_type_description {
my ($type) = @_;
my %type_map = (
'freebsd' => 'FreeBSD',
'freebsd-ufs' => 'FreeBSD UFS',
'freebsd-swap' => 'FreeBSD Swap',
'freebsd-vinum' => 'FreeBSD Vinum',
'freebsd-zfs' => 'FreeBSD ZFS',
'freebsd-boot' => 'FreeBSD Boot',
'efi' => 'EFI System',
'bios-boot' => 'BIOS Boot',
'ms-basic-data' => 'Microsoft Basic Data (FAT/NTFS/exFAT)',
'ms-reserved' => 'Microsoft Reserved',
'ms-recovery' => 'Microsoft Recovery',
'apple-ufs' => 'Apple UFS',
'apple-hfs' => 'Apple HFS',
'apple-boot' => 'Apple Boot',
'apple-raid' => 'Apple RAID',
'apple-label' => 'Apple Label',
'linux-data' => 'Linux Data (ext2/3/4, xfs, btrfs, etc.)',
'linux-swap' => 'Linux Swap',
'linux-lvm' => 'Linux LVM',
'linux-raid' => 'Linux RAID',
);
return $type_map{$type} || $type;
}
sub get_disk_structure {
my ($device) = @_;
my $result = { 'entries' => [], 'partitions' => {} };
my $cmd = "gpart show -l " . quote_path($device) . " 2>&1";
my $out = backquote_command($cmd);
if ( $out =~ /=>\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+\(([^)]+)\)/ ) {
my $start_block = $1; # starting block
my $size_blocks = $2; # number of blocks
$result->{'total_blocks'} =
$start_block + $size_blocks; # last addressable block + 1
$result->{'device_name'} = $3; # device name (e.g., da0)
$result->{'scheme'} = $4; # GPT/MBR
$result->{'size_human'} = $5; # human size from header
}
foreach my $line ( split( /\n/, $out ) ) {
# Free space rows
if ( $line =~ /^\s+(\d+)\s+(\d+)\s+-\s+free\s+-\s+\(([^)]+)\)/ ) {
push @{ $result->{'entries'} },
{
'start' => $1,
'size' => $2,
'size_human' => $3,
'type' => 'free'
};
next;
}
# Partition rows from `gpart show -l` have: start size index label
# [flags] (size_human)
# Some systems include optional tokens like "[active]" after the label.
# Accept them.
if ( $line =~
/^\s+(\d+)\s+(\d+)\s+(\d+)\s+(\S+)(?:\s+\[[^\]]+\])?\s+\(([^)]+)\)/
)
{
push @{ $result->{'entries'} },
{
'start' => $1,
'size' => $2,
'index' => $3,
'label' => $4,
'size_human' => $5,
'type' => 'partition'
};
}
}
# Merge additional info from 'gpart list' directly, keyed by Name -> index
my $list_out =
backquote_command( "gpart list " . quote_path($device) . " 2>&1" );
my ( %parts, $current_idx );
foreach my $line ( split( /\n/, $list_out ) ) {
if ( $line =~ /^\s*(?:\d+\.\s*)?Name:\s*(\S+)/i ) {
my $name = $1; # e.g., da0p2 or da0s2
if ( $name =~ /[ps](\d+)$/ ) {
$current_idx = int($1);
$parts{$current_idx} ||= { name => $name };
}
else {
undef $current_idx; # not a partition provider line
}
}
elsif ( defined $current_idx && $line =~ /^\s*Index:\s*(\d+)/i ) {
# Optional cross-check; ignore value and trust Name-derived index
next;
}
elsif ( defined $current_idx && $line =~ /^\s*label:\s*(\S+)/i ) {
$parts{$current_idx}->{'label'} = $1;
}
elsif ( defined $current_idx && $line =~ /^\s*type:\s*(\S+)/i ) {
$parts{$current_idx}->{'type'} = $1;
}
elsif ( defined $current_idx && $line =~ /^\s*rawtype:\s*(\S+)/i ) {
$parts{$current_idx}->{'rawtype'} = $1;
}
elsif ( defined $current_idx && $line =~ /^\s*length:\s*(\d+)/i ) {
$parts{$current_idx}->{'length'} = $1;
}
elsif ( defined $current_idx && $line =~ /^\s*offset:\s*(\d+)/i ) {
$parts{$current_idx}->{'offset'} = $1;
}
elsif ( $line =~ /Sectorsize:\s*(\d+)/i ) {
$result->{'sectorsize'} = int($1);
}
elsif ( $line =~ /Mediasize:\s*(\d+)/i ) {
$result->{'mediasize'} = int($1);
}
}
$result->{'partitions'} = \%parts;
foreach my $entry ( @{ $result->{'entries'} } ) {
next unless ( $entry->{'type'} eq 'partition' && $entry->{'index'} );
my $idx = $entry->{'index'};
if ( $parts{$idx} ) {
# Prefer label from gpart list if present and meaningful
if ( $parts{$idx}->{'label'}
&& $parts{$idx}->{'label'} ne '(null)' )
{
$entry->{'label'} = $parts{$idx}->{'label'};
}
# Attach resolved type (rawtype/type) for downstream consumers
$entry->{'part_type'} =
$parts{$idx}->{'type'}
|| $parts{$idx}->{'rawtype'}
|| $entry->{'part_type'};
# Also store rawtype for downstream consumers
$entry->{'rawtype'} = $parts{$idx}->{'rawtype'}
if ( $parts{$idx}->{'rawtype'} );
}
}
return $result;
}
sub get_disk_sectorsize {
my ($device) = @_;
# Normalize device for diskinfo (expects provider name like da0)
my $dev = $device;
$dev =~ s{^/dev/}{};
# Prefer verbose output which explicitly lists sectorsize
my $outv =
backquote_command( "diskinfo -v " . quote_path($dev) . " 2>/dev/null" );
if ( $outv =~ /sectorsize:\s*(\d+)/i ) {
return int($1);
}
# Fallback to non-verbose; actual format: name sectorsize mediasize ...
my $out =
backquote_command( "diskinfo " . quote_path($dev) . " 2>/dev/null" );
if ( $out =~ /^\S+\s+(\d+)\s+\d+/ ) {
return int($1); # second field is sectorsize
}
# Last resort: ask gpart list for sectorsize
my $base = $dev;
my $ds = get_disk_structure($base);
if ( $ds && $ds->{'sectorsize'} ) { return int( $ds->{'sectorsize'} ); }
return undef;
}
# Derive the base disk device (e.g., /dev/da0 from /dev/da0p2)
sub base_disk_device {
my ($device) = @_;
return undef unless $device;
my $d = $device;
$d =~ s{^/dev/}{};
$d =~ s{(p|s)\d+.*$}{}; # strip partition/slice suffix
return "/dev/$d";
}
# Compute bytes from a block count for a given device
sub bytes_from_blocks {
my ( $device, $blocks ) = @_;
return undef unless defined $blocks;
my $base = base_disk_device($device) || $device;
my $ss = get_disk_sectorsize($base) || 512;
return $blocks * $ss;
}
# Safe wrapper for nice_size that ensures bytes input
# Accepts either raw bytes or (device, blocks) pair
sub safe_nice_size {
my ( $arg1, $arg2 ) = @_;
my $bytes;
if ( defined $arg2 ) {
# Called as (device, blocks)
$bytes = bytes_from_blocks( $arg1, $arg2 );
}
else {
# Called as (bytes)
$bytes = $arg1;
}
return '-' unless defined $bytes && $bytes >= 0;
my $s = nice_size($bytes);
# Normalize IEC suffixes to SI-style labels if present
$s =~ s/\bKiB\b/KB/g;
$s =~ s/\bMiB\b/MB/g;
$s =~ s/\bGiB\b/GB/g;
$s =~ s/\bTiB\b/TB/g;
$s =~ s/\bPiB\b/PB/g;
$s =~ s/\bEiB\b/EB/g;
return $s;
}
sub build_zfs_devices_cache {
my %pools;
my %devices;
my $cmd = "zpool status 2>&1";
my $out = backquote_command($cmd);
my (
$current_pool, $in_config, $current_vdev_type,
$current_vdev_group, $is_mirrored, $is_raidz,
$raidz_level, $is_single, $is_striped,
$vdev_count
);
$current_vdev_type = 'data';
foreach my $line ( split( /\n/, $out ) ) {
if ( $line =~ /^\s*pool:\s+(\S+)/ ) {
$current_pool = $1;
$pools{$current_pool} = 1;
$in_config = 0;
$current_vdev_type = 'data';
}
elsif ( $line =~ /^\s*config:/ ) {
$in_config = 1;
$current_vdev_group = undef;
$is_mirrored = 0;
$is_raidz = 0;
$raidz_level = 0;
$is_single = 0;
$is_striped = 0;
$vdev_count = 0;
}
elsif ( $in_config and $line =~ /^\s+logs/ ) {
$current_vdev_type = 'log';
$current_vdev_group = undef;
}
elsif ( $in_config and $line =~ /^\s+cache/ ) {
$current_vdev_type = 'cache';
$current_vdev_group = undef;
}
elsif ( $in_config and $line =~ /^\s+spares/ ) {
$current_vdev_type = 'spare';
$current_vdev_group = undef;
}
elsif ( $in_config and $line =~ /^\s+mirror-(\d+)/ ) {
$current_vdev_group = "mirror-$1";
$is_mirrored = 1;
$is_raidz = 0;
$is_single = 0;
$is_striped = 0;
$vdev_count = 0;
}
elsif ( $in_config and $line =~ /^\s+raidz(\d+)?-(\d+)/ ) {
$current_vdev_group = "raidz" . ( $1 || "1" ) . "-$2";
$is_mirrored = 0;
$is_raidz = 1;
$raidz_level = $1 || 1;
$is_single = 0;
$is_striped = 0;
$vdev_count = 0;
}
elsif ( $in_config and $line =~ /^\s+(\S+)\s+(\S+)/ ) {
my $device = $1;
my $state = $2;
next
if ( $device eq $current_pool
or $device =~ /^mirror-/
or $device =~ /^raidz\d*-/ );
if ($current_vdev_group) { $vdev_count++; }
else { $is_single = 1; }
my $device_id = $device;
$device_id = $1 if ( $device =~ /^gpt\/(.*)/ );
$devices{$device} = {
'pool' => $current_pool,
'vdev_type' => $current_vdev_type,
'is_mirrored' => $is_mirrored,
'is_raidz' => $is_raidz,
'raidz_level' => $raidz_level,
'is_single' => $is_single,
'is_striped' => $is_striped,
'vdev_group' => $current_vdev_group,
'vdev_count' => $vdev_count
};
$devices{"gpt/$device"} = $devices{$device}
if ( $device !~ /^gpt\// );
$devices{"/dev/$device"} = $devices{$device};
if ( $device !~ /^gpt\// ) {
$devices{"/dev/gpt/$device"} = $devices{$device};
}
$devices{ lc($device) } = $devices{$device};
if ( $device !~ /^gpt\// ) {
$devices{ "gpt/" . lc($device) } = $devices{$device};
$devices{ "/dev/gpt/" . lc($device) } = $devices{$device};
}
}
}
return ( \%pools, \%devices );
}
sub get_format_type {
my ($part) = @_;
if ( $part->{'type'} =~ /^freebsd-/ ) {
return get_type_description( $part->{'type'} );
}
return get_type_description( $part->{'type'} ) || $part->{'type'};
}
# Build possible ids for a partition given base device, scheme and metadata
sub _label_conflicts_with_device {
my ( $label, $base_device, $scheme, $part_num, $part_name ) = @_;
return 0 unless defined $label && $label ne '-' && $label ne '(null)';
my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's';
my $expected =
( defined $base_device && defined $part_num && length($base_device) )
? "$base_device$sep$part_num"
: undef;
if ( defined $expected && lc($label) eq lc($expected) ) {
return 0;
}
if ( defined $part_name
&& $part_name ne '-'
&& lc($label) eq lc($part_name) )
{
return 0;
}
# If a label looks like a real disk partition but doesn't match this partition,
# Do not use it for ZFS membership detection (avoids cross-disk
# misidentification).
if ( $label =~ m{^(ada|ad|da|amrd|nvd|vtbd)\d+(?:p|s)\d+$}i ) {
return 1;
}
return 0;
}
sub _possible_partition_ids {
my ( $base_device, $scheme, $part_num, $part_name, $part_label ) = @_;
my @ids;
if ( defined $base_device && defined $part_num && length($base_device) ) {
my $sep = ( $scheme && $scheme eq 'GPT' ) ? 'p' : 's';
my $device_path = "/dev/$base_device" . $sep . $part_num;
push( @ids, $device_path );
( my $short = $device_path ) =~ s/^\/dev\///;
push( @ids, $short );
}
if ( $part_name && $part_name ne '-' ) {
push( @ids, $part_name, "/dev/$part_name" );
}
if ( defined $part_label && $part_label ne '-' && $part_label ne '(null)' )
{
my $label_conflict =
_label_conflicts_with_device( $part_label, $base_device, $scheme,
$part_num, $part_name );
if ($label_conflict) {
# Allow only GPT label aliases to avoid false matches to device-like labels
push( @ids,
"gpt/$part_label", "/dev/gpt/$part_label",
"gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) );
}
else {
push( @ids,
$part_label, "gpt/$part_label",
"/dev/gpt/$part_label", lc($part_label),
"gpt/" . lc($part_label), "/dev/gpt/" . lc($part_label) );
}
if ( $part_label =~ /^(sLOG\w+)$/ ) {
push( @ids, $1, "gpt/$1", "/dev/gpt/$1" );
}
}
return @ids;
}
# Given ids, find if present in ZFS devices cache
sub _find_in_zfs {
my ( $zfs_devices, @ids ) = @_;
foreach my $id (@ids) {
my $nid = lc($id);
if ( $zfs_devices->{$nid} ) {
return $zfs_devices->{$nid};
}
}
return undef;
}
# Classify a partition row: returns (format, usage, role)
sub classify_partition_row {
my (%args) = @_;
my $ids = [
_possible_partition_ids(
@args{qw/base_device scheme part_num part_name part_label/}
)
];
my $zdev = _find_in_zfs( $args{'zfs_devices'}, @$ids );
# Derive type description, avoid label-as-type
my $type_desc = $args{'entry_part_type'};
if ( !defined $type_desc || $type_desc eq '-' || $type_desc eq 'unknown' ) {
# leave undef
}
# Avoid clearing real types (like 'efi' or 'freebsd-boot') when label
# text matches by case.
# Only drop if the "type" clearly looks like a provider/label path that
# mirrors the label.
if ( defined $type_desc && defined $args{'part_label'} ) {
my $pl = $args{'part_label'};
if ( $type_desc =~ m{^(?:/dev/)?gpt(?:id)?/\Q$pl\E$}i ) {
undef $type_desc;
}
}
my ( $format, $usage, $role ) = ( '-', $text{'part_nouse'}, '-' );
# Explicit boot detection based on GPT GUIDs and MBR hex codes or
# human-readable type
my $raw = lc( $args{'entry_rawtype'} || '' );
my $t = lc( $type_desc || '' );
my %boot_guid = map { $_ => 1 } qw(
c12a7328-f81f-11d2-ba4b-00a0c93ec93b # EFI System
21686148-6449-6e6f-744e-656564454649 # BIOS Boot (GRUB BIOS)
83bd6b9d-7f41-11dc-be0b-001560b84f0f # FreeBSD Boot
49f48d5a-b10e-11dc-b99b-0019d1879648 # NetBSD boot
824cc7a0-36a8-11e3-890a-952519ad3f61 # OpenBSD boot
426f6f74-0000-11aa-aa11-00306543ecac # Apple Boot
);
my %boot_mbr = map { $_ => 1 } qw( 0xef 0xa0 0xa5 0xa6 0xa9 0xab );
my $is_boot_type =
( $t =~
/\b(efi|bios-?boot|freebsd-boot|netbsd-boot|openbsd-boot|apple-boot)\b/
);
my $is_boot_raw = ( $raw && ( $boot_guid{$raw} || $boot_mbr{$raw} ) );
if ( $is_boot_type || $is_boot_raw ) {
my $fmt =
( $t =~ /efi/
|| $raw eq 'c12a7328-f81f-11d2-ba4b-00a0c93ec93b'
|| lc($raw) eq '0xef' )
? get_type_description('efi')
: get_type_description('freebsd-boot');
# Access text properly - %text is in main namespace when this is called from CGI
my $boot_txt = $text{'disk_boot'};
my $role_txt = $text{'disk_boot_role'};
return ( $fmt, $boot_txt, $role_txt );
}
# Heuristic fallback only if no explicit identifiers are present
if (
(
!$args{'entry_part_type'}
|| $args{'entry_part_type'} eq '-'
|| $args{'entry_part_type'} eq 'unknown'
)
&& ( $args{'part_num'} || '' ) eq '1'
)
{
my $sb = $args{'size_blocks'} || 0;
if ( $sb > 0 ) {
my $base =
base_disk_device( '/dev/' . ( $args{'base_device'} || '' ) );
my $ss = get_disk_sectorsize($base) || 512;
my $bytes = $sb * $ss;
if ( $bytes <= 2 * 1024 * 1024 ) { # <= 2MiB
return ( get_type_description('freebsd-boot'),
$text{'disk_boot'}, $text{'disk_boot_role'} );
}
}
elsif ($args{'size_human'}
&& $args{'size_human'} =~ /^(?:512k|1m|1\.0m)$/i )
{
return ( get_type_description('freebsd-boot'),
$text{'disk_boot'}, $text{'disk_boot_role'} );
}
}
if ($zdev) {
$format = 'FreeBSD ZFS';
my $inzfs_txt = $text{'disk_inzfs'};
my $z_mirror = $text{'disk_zfs_mirror'};
my $z_stripe = $text{'disk_zfs_stripe'};
my $z_single = $text{'disk_zfs_single'};
my $z_data = $text{'disk_zfs_data'};
my $z_log = $text{'disk_zfs_log'};
my $z_cache = $text{'disk_zfs_cache'};
my $z_spare = $text{'disk_zfs_spare'};
$usage = $inzfs_txt . ' ' . $zdev->{'pool'};
my $vt = $zdev->{'vdev_type'};
my $cnt = $zdev->{'vdev_count'} || 0;
if ( $vt eq 'log' ) { $role = $z_log; }
elsif ( $vt eq 'cache' ) { $role = $z_cache; }
elsif ( $vt eq 'spare' ) { $role = $z_spare; }
elsif ( $zdev->{'is_mirrored'} ) {
$role = $z_mirror;
$role .= " ($cnt in group)" if $cnt;
}
elsif ( $zdev->{'is_raidz'} ) {
my $lvl = $zdev->{'raidz_level'} || 1;
$role = 'RAID-Z' . $lvl;
$role .= " ($cnt in group)" if $cnt;
}
elsif ( $zdev->{'is_striped'} ) {
$role = $z_stripe;
$role .= " ($cnt in group)" if $cnt;
}
elsif ( $zdev->{'is_single'} ) { $role = $z_single; }
else { $role = $z_data; }
return ( $format, $usage, $role );
}
# Not in ZFS: infer by type_desc, rawtype and size heuristic (this
# shouldn't be reached for boot, but keep as fallback)
if ( defined $type_desc && $type_desc =~ /(?:freebsd|linux)-swap/i ) {
$format = 'Swap';
$usage = $text{'disk_swap'};
$role = $text{'disk_swap_role'};
}
elsif ( defined $type_desc && $type_desc =~ /linux-lvm/i ) {
$format = 'Linux LVM';
}
elsif ( defined $type_desc && $type_desc =~ /linux-raid/i ) {
$format = 'Linux RAID';
}
# Recognize common FAT/NTFS identifiers on both GPT and MBR
elsif ( defined $type_desc && $type_desc =~ /ntfs/i ) { $format = 'NTFS'; }
elsif ( defined $type_desc && $type_desc =~ /fat32/i ) {
$format = 'FAT32';
}
elsif ( defined $type_desc && $type_desc =~ /fat|msdos/i ) {
$format = 'FAT';
}
elsif ( defined $type_desc && $type_desc =~ /ms-basic/i ) {
$format = 'FAT/NTFS';
}
elsif ( $raw ne '' && $raw =~ /^\d+$/ ) {
# MBR raw type codes: 7=NTFS, 6/11/12/14 = FAT variants
my $code = int($raw);
if ( $code == 7 ) { $format = 'NTFS'; }
elsif ( $code == 11 || $code == 12 ) { $format = 'FAT32'; }
elsif ( $code == 6 || $code == 14 ) { $format = 'FAT'; }
}
elsif ( defined $type_desc && $type_desc =~ /linux/i ) {
$format = 'Linux';
}
elsif ( defined $type_desc && $type_desc =~ /apple-ufs/i ) {
$format = 'Apple UFS';
}
elsif ( defined $type_desc && $type_desc =~ /apple-hfs/i ) {
$format = 'HFS+';
}
elsif ( defined $type_desc && $type_desc =~ /apple-raid/i ) {
$format = 'Apple RAID';
}
elsif ( defined $type_desc && $type_desc =~ /freebsd-ufs/i ) {
$format = 'FreeBSD UFS';
}
elsif ( defined $type_desc && $type_desc =~ /freebsd-zfs/i ) {
$format = 'FreeBSD ZFS';
}
else {
if ( defined $args{'part_label'}
&& $args{'part_label'} =~ /^swap\d*$/i )
{
$format = 'Swap';
$usage = $text{'disk_swap'};
$role = $text{'disk_swap_role'};
}
}
return ( $format, $usage, $role );
}
# list_partition_types()
# Returns a list suitable for ui_select: [ [ value, label ], ... ]
# Adapts to whether the system uses gpart (GPT) or legacy disklabel.
sub list_partition_types {
my ($scheme) = @_;
if ( is_using_gpart() ) {
# BSD-on-MBR inner label (used when creating sub-partitions inside an MBR slice)
if ( defined $scheme && $scheme =~ /BSD/i ) {
return (
[ 'freebsd-ufs', get_type_description('freebsd-ufs') ],
[ 'freebsd-zfs', get_type_description('freebsd-zfs') ],
[ 'freebsd-swap', get_type_description('freebsd-swap') ],
[ 'freebsd-vinum', get_type_description('freebsd-vinum') ],
);
}
# If outer scheme is not GPT (e.g. MBR), present MBR partition types for
# top-level slices
if ( defined $scheme && $scheme !~ /GPT/i ) {
my @mbr_types = (
[ 'freebsd', get_type_description('freebsd') ],
[ 'fat32lba', 'FAT32 (LBA)' ],
[ 'fat32', 'FAT32' ],
[ 'fat16', 'FAT16' ],
[ 'ntfs', 'NTFS' ],
[ 'linux', 'Linux' ],
[ 'linux-swap', get_type_description('linux-swap') ],
[ 'efi', get_type_description('efi') ],
);
return @mbr_types;
}
# Default GPT types (common, practical choices)
my @gpt_types = (
[ 'efi', get_type_description('efi') ],
[ 'bios-boot', get_type_description('bios-boot') ],
[ 'freebsd-boot', get_type_description('freebsd-boot') ],
[ 'freebsd-zfs', get_type_description('freebsd-zfs') ],
[ 'freebsd-ufs', get_type_description('freebsd-ufs') ],
[ 'freebsd-swap', get_type_description('freebsd-swap') ],
[ 'freebsd-vinum', get_type_description('freebsd-vinum') ],
[ 'ms-basic-data', get_type_description('ms-basic-data') ],
[ 'ms-reserved', get_type_description('ms-reserved') ],
[ 'ms-recovery', get_type_description('ms-recovery') ],
[ 'linux-data', get_type_description('linux-data') ],
[ 'linux-swap', get_type_description('linux-swap') ],
[ 'linux-lvm', get_type_description('linux-lvm') ],
[ 'linux-raid', get_type_description('linux-raid') ],
[ 'apple-boot', get_type_description('apple-boot') ],
[ 'apple-hfs', get_type_description('apple-hfs') ],
[ 'apple-ufs', get_type_description('apple-ufs') ],
[ 'apple-raid', get_type_description('apple-raid') ],
[ 'apple-label', get_type_description('apple-label') ],
);
return @gpt_types;
}
else {
# Legacy BSD disklabel types
my @label_types = (
[ '4.2BSD', 'FreeBSD UFS' ],
[ 'swap', 'Swap' ],
[ 'unused', 'Unused' ],
[ 'vinum', 'FreeBSD Vinum' ],
);
return @label_types;
}
}
sub get_partition_role {
my ($part) = @_;
if ( is_boot_partition($part) ) { return $text{'part_boot'}; }
my $zfs_info = get_all_zfs_info();
if ( $zfs_info->{ $part->{'device'} }
and $zfs_info->{ $part->{'device'} } =~ /\(log\)$/ )
{
return $text{'part_zfslog'};
}
if ( $zfs_info->{ $part->{'device'} } ) {
return $text{'part_zfsdata'};
}
my @mounts = mount::list_mounted();
foreach my $m (@mounts) {
if ( $m->[0] eq $part->{'device'} ) {
return text( 'part_mounted', $m->[1] );
}
}
return $text{'part_unused'};
}
sub get_detailed_disk_info {
my ($device) = @_;
my $info = {};
( my $dev_name = $device ) =~ s/^\/dev\///;
my $out = backquote_command(
"geom disk list " . quote_path($dev_name) . " 2>/dev/null" );
return undef if ($?);
foreach my $line ( split( /\n/, $out ) ) {
if ( $line =~ /^\s+Mediasize:\s+(\d+)\s+\(([^)]+)\)/ ) {
$info->{'mediasize_bytes'} = $1;
$info->{'mediasize'} = $2;
}
elsif ( $line =~ /^\s+Sectorsize:\s+(\d+)/ ) {
$info->{'sectorsize'} = $1;
}
elsif ( $line =~ /^\s+Stripesize:\s+(\d+)/ ) {
$info->{'stripesize'} = $1;
}
elsif ( $line =~ /^\s+Stripeoffset:\s+(\d+)/ ) {
$info->{'stripeoffset'} = $1;
}
elsif ( $line =~ /^\s+Mode:\s+(.*)/ ) {
$info->{'mode'} = $1;
}
elsif ( $line =~ /^\s+rotationrate:\s+(\d+)/ ) {
$info->{'rotationrate'} = $1;
}
elsif ( $line =~ /^\s+ident:\s+(.*)/ ) {
$info->{'ident'} = $1;
}
elsif ( $line =~ /^\s+lunid:\s+(.*)/ ) {
$info->{'lunid'} = $1;
}
elsif ( $line =~ /^\s+descr:\s+(.*)/ ) {
$info->{'descr'} = $1;
}
}
return $info;
}
sub initialize_slice {
my ( $disk, $slice ) = @_;
# If the outer disk is GPT, we are not creating inner BSD labels
if ( is_using_gpart() ) {
my $base = $disk->{'device'};
$base =~ s{^/dev/}{};
my $ds = get_disk_structure($base);
if ( $ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i ) {
return undef;
}
}
# For MBR: initialize BSD disklabel on the slice only if not already present
my $prov = slice_name($slice);
my $show = backquote_command( "gpart show " . quote_path($prov) . " 2>&1" );
return undef if ( $show =~ /\bBSD\b/ );
my $cmd = "gpart create -s BSD " . quote_path($prov) . " 2>&1";
my $out = backquote_command($cmd);
if ( $? != 0 ) {
return "Failed to initialize slice: $out";
}
return undef;
}
# ---------------------------------------------------------------------
# partition_select(name, value)
# Provide a selector for partitions/slices for external modules (e.g., mount)
# Returns an HTML <select> element populated with available devices.
sub partition_select {
my ( $name, $value ) = @_;
my @opts;
my @disks = list_disks_partitions();
foreach my $d (@disks) {
foreach my $s ( @{ $d->{'slices'} || [] } ) {
my $stype = get_type_description( $s->{'type'} ) || $s->{'type'};
my $ssize_b = bytes_from_blocks( $s->{'device'}, $s->{'blocks'} );
my $ssize = defined $ssize_b ? nice_size($ssize_b) : undef;
my $slabel = $s->{'device'}
. ( defined $ssize ? " ($stype, $ssize)" : " ($stype)" );
push @opts, [ $s->{'device'}, $slabel ];
foreach my $p ( @{ $s->{'parts'} || [] } ) {
my $ptype =
get_type_description( $p->{'type'} ) || $p->{'type'};
my $psz_b = bytes_from_blocks( $p->{'device'}, $p->{'blocks'} );
my $psz = defined $psz_b ? nice_size($psz_b) : undef;
my $plabel = $p->{'device'}
. ( defined $psz ? " ($ptype, $psz)" : " ($ptype)" );
push @opts, [ $p->{'device'}, $plabel ];
}
}
}
# Sort options by device name for consistency
@opts = sort { $a->[0] cmp $b->[0] } @opts;
$value ||= ( $opts[0] ? $opts[0]->[0] : undef );
return ui_select( $name, $value, \@opts, 1, 0, 0, 0 );
}
# Return a short human-readable description for a given device
# Example: "FreeBSD ZFS, 39G"
sub partition_description {
my ($device) = @_;
return undef unless $device;
my ( $ptype, $blocks );
my @disks = list_disks_partitions();
foreach my $d (@disks) {
foreach my $s ( @{ $d->{'slices'} || [] } ) {
if ( $s->{'device'} && $s->{'device'} eq $device ) {
$ptype = get_type_description( $s->{'type'} ) || $s->{'type'};
$blocks = $s->{'blocks'};
last;
}
foreach my $p ( @{ $s->{'parts'} || [] } ) {
if ( $p->{'device'} && $p->{'device'} eq $device ) {
$ptype =
get_type_description( $p->{'type'} ) || $p->{'type'};
$blocks = $p->{'blocks'};
last;
}
}
}
}
return undef unless defined $ptype;
my $bytes = bytes_from_blocks( $device, $blocks );
my $sz = defined $bytes ? nice_size($bytes) : '-';
return "$ptype, $sz";
}
1;