Files
webmin/bsdfdisk/bsdfdisk-lib.pl
karmantyu 032f4447db Add files via upload
gpart edition
2026-01-23 19:41:57 +01:00

1484 lines
59 KiB
Perl
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `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 = `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 = `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 = `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 = `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 = `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 = `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 = `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 = `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 = `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 = `diskinfo $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 = `diskinfo -v $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 = `camcontrol identify $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 = `camcontrol inquiry $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 = `gpart show $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 = `gpart show $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 = `fdisk /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 = `disklabel -r $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'};
}
#---------------------------------------------------------------------
# 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 $base";
my $init_out = `$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'};
}
$cmd = "gpart add -t " . $slice->{'type'};
$cmd .= " -b $slice->{'startblock'}" if ($slice->{'startblock'});
$cmd .= " -s $slice->{'blocks'}" if ($slice->{'blocks'});
$cmd .= " " . $base;
my $out = `$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 {
$cmd = "fdisk -a";
$cmd .= " -s $slice->{'number'}" if ($slice->{'number'});
$cmd .= " -b $slice->{'startblock'}" if ($slice->{'startblock'});
$cmd .= " -s $slice->{'blocks'}" if ($slice->{'blocks'});
$cmd .= " -t $slice->{'type'} " . $disk->{'device'};
my $out = `$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()) {
$cmd = "gpart delete -i " . slice_number($slice) . " " . disk_name($disk->{'device'});
my $out = `$cmd 2>&1`;
return ($?) ? $out : undef;
} else {
$cmd = "fdisk -d " . $slice->{'number'} . " " . $disk->{'device'};
my $out = `$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 $idx = (ord($part->{'letter'}) - ord('a')) + 1;
$cmd = "gpart delete -i $idx " . slice_name($slice);
my $out = `$cmd 2>&1`;
return ($?) ? $out : undef;
} else {
$cmd = "disklabel -r -w -d $part->{'letter'} " . $slice->{'device'};
my $out = `$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()) {
$cmd = "gpart modify -i " . slice_number($slice) . " -t " . $slice->{'type'} . " " . disk_name($disk->{'device'});
my $out = `$cmd 2>&1`;
return ($?) ? $out : undef;
} else {
$cmd = "fdisk -a -s " . $slice->{'number'} . " -t " . $slice->{'type'} . " " . $disk->{'device'};
my $out = `$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 $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 $idx = (ord($part->{'letter'}) - ord('a')) + 1;
$cmd = "gpart modify -i $idx -t " . $part->{'type'} . " $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 = `$cmd 2>&1`;
return ($?) ? $out : undef;
} else {
$cmd = "disklabel -r -w -p " . $part->{'letter'} . " -t " . $part->{'type'} . " " . $slice->{'device'};
my $out = `$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 $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 $prov 2>&1");
}
# Compute 1-based index
my $idx = (ord($part->{'letter'}) - ord('a')) + 1;
# For BSD disklabel, start blocks are ALWAYS slice-relative
# BSD partitions use 0-based addressing within the slice
my $start_rel = $part->{'startblock'};
my $blocks = $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 .= " $prov";
my $out = `$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) . " $base";
my $out = `$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 = `glabel destroy $old_label 2>&1`;
}
my $cmd = "glabel label " . quote_path($label) . " " . quote_path($device);
my $out = `$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 \"\" $base";
my $out = `$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 = `$cmd 2>&1`;
return ($? ? $out : undef);
}
}
return undef;
}
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 $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|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;
}
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);
# Map to specific fsck tools when available; else use fsck -t
if ($fstype && $fstype eq 'ufs') {
return has_command('fsck_ufs') ? "fsck_ufs -y $device" : "fsck -t ufs -y $device";
}
if ($fstype && $fstype eq 'msdosfs') {
return has_command('fsck_msdosfs') ? "fsck_msdosfs -y $device" : "fsck -t msdosfs -y $device";
}
if ($fstype && $fstype eq 'ext2fs') {
return has_command('fsck_ext2fs') ? "fsck_ext2fs -y $device" : "fsck -t ext2fs -y $device";
}
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 $device";
}
sub show_filesystem_buttons {
my ($hiddens, $st, $object) = @_;
# 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);
# Do not offer fsck for swap or ZFS devices
my $zmap = get_all_zfs_info();
my $is_swap = (@$st && $st->[1] eq 'swap') || ($object->{'type'} && $object->{'type'} =~ /freebsd-swap|^82$/i);
my $is_zfs = $zmap->{$object->{'device'}} ? 1 : 0;
if ((!@$st || !$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') {
print ui_buttons_row("../mount/edit_mount.cgi", $text{'part_newmount2'}, $text{'part_mountmsg2'},
ui_hidden("newdev", $preferred_dev) . ui_hidden("type", "swap"));
}
else {
print ui_buttons_row("../mount/edit_mount.cgi", $text{'part_newmount'}, $text{'part_mountmsg'},
ui_hidden("newdev", $preferred_dev) . ui_hidden("type", "ufs") . ui_textbox("newdir", undef, 20));
}
}
}
#---------------------------------------------------------------------
# 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',
'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',
'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 $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 $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 $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 $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 _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)') {
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
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 $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 $prov 2>&1");
return undef if ($show =~ /\bBSD\b/);
my $cmd = "gpart create -s BSD $prov 2>&1";
my $out = `$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;