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', "fdisk", "gpart");
}
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