From 032f4447dbad0807c391a0aa22973390db322bff Mon Sep 17 00:00:00 2001 From: karmantyu Date: Fri, 23 Jan 2026 19:41:57 +0100 Subject: [PATCH] Add files via upload gpart edition --- bsdfdisk/bsdfdisk-lib.pl | 1860 ++++++++++++++++++++++++++++--------- bsdfdisk/create_part.cgi | 9 +- bsdfdisk/create_slice.cgi | 144 ++- bsdfdisk/edit_disk.cgi | 402 ++++++-- bsdfdisk/edit_part.cgi | 193 ++-- bsdfdisk/edit_slice.cgi | 298 +++--- bsdfdisk/fsck.cgi | 66 +- bsdfdisk/images/free.gif | Bin 0 -> 44 bytes bsdfdisk/index.cgi | 78 +- bsdfdisk/lang/en | 179 +++- bsdfdisk/newfs.cgi | 42 +- bsdfdisk/newfs_form.cgi | 60 +- bsdfdisk/part_form.cgi | 64 +- bsdfdisk/save_slice.cgi | 19 +- bsdfdisk/slice_form.cgi | 124 ++- bsdfdisk/smart.cgi | 31 + 16 files changed, 2623 insertions(+), 946 deletions(-) create mode 100644 bsdfdisk/images/free.gif create mode 100644 bsdfdisk/smart.cgi diff --git a/bsdfdisk/bsdfdisk-lib.pl b/bsdfdisk/bsdfdisk-lib.pl index b2d66e1ff..e29e8908e 100644 --- a/bsdfdisk/bsdfdisk-lib.pl +++ b/bsdfdisk/bsdfdisk-lib.pl @@ -1,468 +1,1484 @@ -# Functions for FreeBSD disk management - -use strict; -use warnings; -no warnings 'redefine'; -no warnings 'uninitialized'; -BEGIN { push(@INC, ".."); }; +BEGIN { push(@INC, ".."); } use WebminCore; -&init_config(); -&foreign_require("mount"); -&foreign_require("fdisk"); -our (%text); +init_config(); +foreign_require("mount", "mount-lib.pl"); +foreign_require("fdisk", "fdisk-lib.pl"); -sub check_fdisk -{ -foreach my $cmd ("fdisk", "disklabel") { - if (!&has_command($cmd)) { - return &text('index_ecmd', "$cmd"); - } - } -return undef; +#--------------------------------------------------------------------- +# 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 -sub list_disks_partitions -{ -my @rv; +# 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; -# Iterate over disk devices -foreach my $dev (glob("/dev/ada[0-9]"), glob("/dev/ada[0-9][0-9]"), - glob("/dev/ad[0-9]"), glob("/dev/ad[0-9][0-9]"), - glob("/dev/da[0-9]"), glob("/dev/da[0-9][0-9]")) { - next if (!-r $dev || -l $dev); - my $disk = { 'device' => $dev, - 'prefix' => $dev, - 'type' => $dev =~ /^\/dev\/da/ ? 'scsi' : 'ide', - 'slices' => [ ] }; - if ($dev =~ /^\/dev\/(.*)/) { - $disk->{'short'} = $1; - } - if ($dev =~ /^\/dev\/([a-z]+)(\d+)/) { - $disk->{'number'} = $2; - $disk->{'desc'} = &text('select_device', - uc($disk->{'type'}), "$2"); - } - $disk->{'index'} = scalar(@rv); - push(@rv, $disk); + # 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 size and slices - my $out = &backquote_command("fdisk ".quotemeta($dev)); - my @lines = split(/\r?\n/, $out); - my $slice; - for(my $i=0; $i<@lines; $i++) { - if ($lines[$i] =~ /cylinders=(\d+)\s+heads=(\d+)\s+sectors\/track=(\d+)\s+\((\d+)/) { - # Disk information - $disk->{'cylinders'} = $1; - $disk->{'heads'} = $2; - $disk->{'sectors'} = $3; - $disk->{'blksper'} = $4; - $disk->{'blocks'} = $disk->{'cylinders'} * - $disk->{'blksper'}; - $disk->{'blocksize'} = 512; # Guessed? - $disk->{'size'} = $disk->{'blocks'} * - $disk->{'blocksize'}; - } - elsif ($i+1 < @lines && - $lines[$i+1] !~ // && - $lines[$i] =~ /data\s+for\s+partition\s+(\d+)/) { - # Start of a slice - $slice = { 'number' => $1, - 'device' => $dev."s".$1, - 'index' => scalar(@{$disk->{'slices'}}) }; - if ($slice->{'device'} =~ /^\/dev\/([a-z]+)(\d+)s(\d+)/){ - $slice->{'desc'} = &text('select_slice', - uc($disk->{'type'}), "$2", "$3"); - } - push(@{$disk->{'slices'}}, $slice); - } - elsif ($lines[$i] =~ /sysid\s+(\d+)\s+\(0x([0-9a-f]+)/ && $slice) { - # Slice type - $slice->{'type'} = $2; - } - elsif ($lines[$i] =~ /start\s+(\d+),\s+size\s+(\d+)\s+\((.*)\)/ && $slice) { - # Slice start and size - $slice->{'startblock'} = $1; - $slice->{'blocks'} = $2; - $slice->{'size'} = &string_to_size("$3"); - $slice->{'active'} = $lines[$i] =~ /\(active\)/ ? 1 : 0; - } - elsif ($lines[$i] =~ /beg:\s+cyl\s+(\d+)/ && $slice) { - # Slice start - $slice->{'start'} = $1; - } - elsif ($lines[$i] =~ /end:\s+cyl\s+(\d+)/ && $slice) { - # Slice end - $slice->{'end'} = $1; - } - } + # Get mount information once for all devices + my ($mount_info, $mount_list) = get_all_mount_points_cached(); - # Get disk model from dmesg - open(DMESG, ") { - if (/^(\S+):\s+(\S+\s+)?<(.*)>/ && $1 eq $disk->{'short'}) { - $disk->{'model'} = $3; - } - elsif (/^(\S+):\s+(\d+)(\S+)\s+\((\d+)\s+(\d+)\s+byte\s+sectors/ && - $1 eq $disk->{'short'}) { - $disk->{'sectorsize'} = $5; - $disk->{'size'} = &string_to_size("$2 $3"); - } - } - close(DMESG); + 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; - # Get partitions within slices - foreach my $slice (@{$disk->{'slices'}}) { - $slice->{'parts'} = [ ]; - next if (!-e $slice->{'device'}); - my $out = &backquote_command("disklabel ".$slice->{'device'}); - my @lines = split(/\r?\n/, $out); - foreach my $l (@lines) { - if ($l =~ /^\s*([a-z]):\s+(\d+)\s+(\d+)\s+(\S+)/) { - my $part = { 'letter' => $1, - 'blocks' => $2, - 'startblock' => $3, - 'type' => $4, - 'device' =>$slice->{'device'}.$1 }; - $part->{'size'} = $part->{'blocks'} * - $disk->{'blocksize'}; - $part->{'desc'} = &text('select_part', - uc($disk->{'type'}), - $disk->{'number'}, - $slice->{'number'}, - uc($part->{'letter'})); - next if ($part->{'type'} eq 'unused' && - $part->{'startblock'} == 0); - push(@{$slice->{'parts'}}, $part); - } - } - } - } + # 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'; + } -return @rv; + # 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; } -# string_to_size(str) -# Convert a string like 100 Meg to a number in bytes -sub string_to_size -{ -my ($str) = @_; -my ($n, $pfx) = split(/\s+/, $str); -if ($pfx =~ /^b/i) { - return $n; - } -if ($pfx =~ /^k/i) { - return $n * 1024; - } -if ($pfx =~ /^m/i) { - return $n * 1024 * 1024; - } -if ($pfx =~ /^g/i) { - return $n * 1024 * 1024 * 1024; - } -if ($pfx =~ /^t/i) { - return $n * 1024 * 1024 * 1024 * 1024; - } -return undef; +# check_fdisk() – unchanged +sub check_fdisk { + if (!has_command("fdisk") and !has_command("gpart")) { + return text('index_efdisk', "fdisk", "gpart"); + } + return undef; } -# partition_select(name, value, mode, &found, disk-regexp) -# Returns HTML for a selector for a slice. The mode parameter means : -# 1 = disks -# 2 = disks, slices and partitions -# 3 = slices and partitions -sub partition_select -{ -my ($name, $value, $mode, $found, $diskre) = @_; -my @opts; -my @dlist = &list_disks_partitions(); -foreach my $d (@dlist) { - my $dev = $d->{'device'}; - next if ($diskre && $dev !~ /$diskre/); - if ($mode == 1 || $mode == 2) { - push(@opts, [ $dev, &partition_description($dev) ]); - } - if ($mode >= 2) { - foreach my $s (@{$d->{'slices'}}) { - push(@opts, [ $s->{'device'}, - &partition_description($s->{'device'}) ]); - foreach my $p (@{$s->{'parts'}}) { - push(@opts, [ $p->{'device'}, - &partition_description($p->{'device'}) ]); - } - } - } - } -if ($found && &indexof($value, map { $_->[0] } @opts) >= 0) { - $$found = 1; - } -return &ui_select($name, $value, \@opts); +# is_using_gpart() +sub is_using_gpart { + return has_command("gpart") ? 1 : 0; } -# partition_description(device) -# Returns a human-readable description for a device name -sub partition_description -{ -my ($dev) = @_; -if ($dev =~ /^\/dev\/([a-z]+)(\d+)$/) { - # A whole disk of some type - return &text('select_device', - $1 eq 'da' ? 'SCSI' : 'IDE', "$2"); - } -elsif ($dev =~ /^\/dev\/([a-z]+)(\d+)s(\d+)$/) { - # A slice within a disk - return &text('select_slice', - $1 eq 'da' ? 'SCSI' : 'IDE', "$2", "$3"); - } -elsif ($dev =~ /^\/dev\/([a-z]+)(\d+)s(\d+)([a-z])$/) { - # A partition within a slice - return &text('select_part', - $1 eq 'da' ? 'SCSI' : 'IDE', "$2", "$3", uc($4)); - } -else { - # No idea - return $dev; - } +# disk_name(device) – extracts name from /dev/device +sub disk_name { + my ($device) = @_; + $device =~ s/^\/dev\///; + return $device; } -# execute_fdisk_commands(&disk, &commands) -# Run a series of commands on a disk via the fdisk config file -sub execute_fdisk_commands -{ -my ($disk, $cmds) = @_; -my $temp = &transname(); -my $fh = "TEMP"; -&open_tempfile($fh, ">$temp"); -foreach my $c (@$cmds) { - &print_tempfile($fh, $c."\n"); - } -&close_tempfile($fh); -my $out = &backquote_logged("fdisk -f $temp $disk->{'device'} &1"); -my $ex = $?; -&unlink_file($temp); -return $ex ? $out : undef; +# slice_name(slice) +sub slice_name { + my ($slice) = @_; + if ($slice->{'device'} =~ /\/dev\/(\S+)/) { + return $1; + } + return $slice->{'number'}; } -# delete_slice(&disk, &slice) -# Delete one slice from a disk -sub delete_slice -{ -my ($disk, $slice) = @_; -return &execute_fdisk_commands($disk, - [ "p $slice->{'number'} 0 0 0" ]); +# 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'}; } -# create_slice(&disk, &slice) -# Add a slice to a disk -sub create_slice -{ -my ($disk, $slice) = @_; -my $type = hex($slice->{'type'}); -my $start = int($slice->{'startblock'} * $disk->{'blocksize'} / 1024); -my $length = int($slice->{'blocks'} * $disk->{'blocksize'} / 1024); -my $err = &execute_fdisk_commands($disk, - [ "p $slice->{'number'} $type ${start}K ${length}K" ]); -if (!$err) { - $slice->{'device'} = $disk->{'device'}."s".$slice->{'number'}; - } -return $err; + +#--------------------------------------------------------------------- +# 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; + } } -# modify_slice(&disk, &old-slice, &slice) -# Apply type or size changes to a slice -sub modify_slice -{ -my ($disk, $oldslice, $slice) = @_; -if ($oldslice->{'type'} ne $slice->{'type'}) { - # Change the type - my $type = hex($slice->{'type'}); - my $start = int(($slice->{'startblock'} * $disk->{'blocksize'}) / 1024); - my $end = int((($slice->{'startblock'} + $slice->{'blocks'}) * - $disk->{'blocksize'}) / 1024); - my $err = &execute_fdisk_commands($disk, - [ "p $slice->{'number'} $type ${start}K ${end}K" ]); - return $err if ($err); - } -if (!$oldslice->{'active'} && $slice->{'active'}) { - # Make active - my $err = &execute_fdisk_commands($disk, - [ "a $slice->{'number'}" ]); - return $err if ($err); - } -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; + } } -# initialize_slice(&disk, &slice) -# After a slice is created, put a default label on it -sub initialize_slice -{ -my ($disk, $slice) = @_; -my $err = &backquote_logged("bsdlabel -w $slice->{'device'}"); -return $? ? $err : 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 list_partition_types -{ -return ( '4.2BSD', 'swap', 'unused', 'vinum' ); +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; + } } -# save_partition(&disk, &slice, &part) -# Create or update a partition on some slice -sub save_partition -{ -my ($disk, $slice, $part) = @_; -my $out = &backquote_command("bsdlabel $slice->{'device'}"); -if ($? && $out =~ /no\s+valid\s+label/) { - # No label at all yet .. initialize - my $err = &initialize_slice($disk, $slice); - return "Failed to create initial disk label : $err" if ($err); - } - -# Edit or add a line in the existing label -my $wantline = " ".$part->{'letter'}.": ".$part->{'blocks'}." ". - $part->{'startblock'}." ".$part->{'type'}; -my @lines = split(/\r?\n/, $out); -my $found = 0; -for(my $i=0; $i<@lines; $i++) { - if ($lines[$i] =~ /^\s+(\S+):/ && $1 eq $part->{'letter'}) { - $lines[$i] = $wantline; - $found++; - last; - } - } -if (!$found) { - push(@lines, $wantline); - } -my $err = &save_partition_lines($slice, \@lines); -if (!$err && !$part->{'device'}) { - $part->{'device'} = $slice->{'device'}.$part->{'letter'}; - } -return $err; +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; + } } -# delete_partition(&disk, &slice, &part) -# Delete a partition on some slice -sub delete_partition -{ -my ($disk, $slice, $part) = @_; - -# Fix up the line for the part being deleted -my $out = &backquote_command("bsdlabel $slice->{'device'}"); -my @lines = split(/\r?\n/, $out); -my $found = 0; -for(my $i=0; $i<@lines; $i++) { - if ($lines[$i] =~ /^\s+(\S+):/ && $1 eq $part->{'letter'}) { - splice(@lines, $i, 1); - } - } -return &save_partition_lines($slice, \@lines); +# 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; } -# save_partition_lines(&slice, &lines) -# Feed the given lines to the bsdlabel command to update a slice's partition -# list. Returns undef on success or an error message on failure. -sub save_partition_lines -{ -my ($slice, $lines) = @_; - -# Write to a temp file -my $fh = "TEMP"; -my $temp = &transname(); -&open_tempfile($fh, ">$temp"); -foreach my $l (@$lines) { - &print_tempfile($fh, $l."\n"); - } -&close_tempfile($fh); - -# Apply the new label -my $out = &backquote_logged("bsdlabel -R $slice->{'device'} $temp"); -my $ex = $?; -&unlink_file($temp); -return $ex ? $out : 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); } -# create_filesystem(&disk, &slice, &part, &fs-details) -# Creates a new filesystem, and returns undef on success or the error output -# on failure. -sub create_filesystem -{ -my ($disk, $slice, $part, $newfs) = @_; -my $cmd = &get_create_filesystem_command($disk, $slice, $part, $newfs); -my $out = &backquote_logged("$cmd 2>&1 {'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; } -# get_create_filesystem_command(&disk, &slice, &part, &fs-details) -# Returns the command to create a new filesystem on some partition -sub get_create_filesystem_command -{ -my ($disk, $slice, $part, $newfs) = @_; -my @cmd = "newfs"; -push(@cmd, "-m", $newfs->{'free'}) if ($newfs->{'free'} ne ''); -push(@cmd, "-t") if ($newfs->{'trim'}); -push(@cmd, "-L", quotemeta($newfs->{'label'})) if ($newfs->{'label'} ne ''); -push(@cmd, $part ? $part->{'device'} : $slice->{'device'}); -return join(" ", @cmd); +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; } -# check_filesystem(&disk, &slice, &part) -# Checks the filesystem on some partition, and returns undef on success or -# the error output on failure. -sub check_filesystem -{ -my ($disk, $slice, $part) = @_; -my $cmd = &get_check_filesystem_command($disk, $slice, $part); -my $out = &backquote_logged("$cmd 2>&1 /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; } -# get_check_filesystem_command(&disk, &slice, &part) -# Returns the command to check a filesystem on some partition -sub get_check_filesystem_command -{ -my ($disk, $slice, $part) = @_; -my $dev = $part ? $part->{'device'} : $slice->{'device'}; -my @cmd = "fsck"; -my @st = &fdisk::device_status($dev); -if (!@st) { - # Assume UFS type - push(@cmd, "-t", "ufs"); - } -push(@cmd, $dev); -return join(" ", @cmd); +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; } -# show_filesystem_buttons(hiddens, &status, &part-or-slice) -# Show buttons to create a filesystem on a partition or slice -sub show_filesystem_buttons -{ -my ($hiddens, $st, $object) = @_; -print &ui_buttons_row( - "newfs_form.cgi", $text{'part_newfs'}, $text{'part_newfsdesc'}, - $hiddens); - -if (!@$st || $st->[1] ne 'swap') { - print &ui_buttons_row( - "fsck.cgi", $text{'part_fsck'}, $text{'part_fsckdesc'}, - $hiddens); - } - -if (!@$st) { - if ($object->{'type'} eq 'swap' || $object->{'type'} eq '82') { - print &ui_buttons_row("../mount/edit_mount.cgi", - $text{'part_newmount2'}, $text{'part_mountmsg2'}, - &ui_hidden("newdev", $object->{'device'}). - &ui_hidden("type", "swap")); - } - else { - print &ui_buttons_row("../mount/edit_mount.cgi", - $text{'part_newmount'}, $text{'part_mountmsg'}, - &ui_hidden("newdev", $object->{'device'}). - &ui_hidden("type", "ufs"), - &ui_textbox("newdir", undef, 20)); - } - } +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"; } -1; +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 ".$text{'nslice_autonext'}."" + : &ui_textbox("number", $n, 6); +print &ui_table_row($text{'nslice_number'}, $num_field); + +# Disk size in blocks (prefer GPART total blocks) +my $disk_blocks = ($disk_structure && $disk_structure->{'total_blocks'}) ? $disk_structure->{'total_blocks'} : ($disk->{'blocks'} || 0); +print &ui_table_row($text{'nslice_diskblocks'}, $disk_blocks); + +# Start and end blocks (defaults to last slice+1). Allow prefill from query. +my ($start, $end) = (2048, $disk_blocks > 0 ? $disk_blocks - 1 : 0); +foreach my $s (sort { $a->{'startblock'} <=> $b->{'startblock'} } @{$disk->{'slices'}}) { - $start = $s->{'startblock'} + $s->{'blocks'} + 1; - } +$start = $s->{'startblock'} + $s->{'blocks'}; # leave 1 block (512B) gap +} +if (defined $in{'start'} && $in{'start'} =~ /^\d+$/) { $start = $in{'start'}; } +if (defined $in{'end'} && $in{'end'} =~ /^\d+$/) { $end = $in{'end'}; } print &ui_table_row($text{'nslice_start'}, - &ui_textbox("start", $start, 10)); +&ui_textbox("start", $start, 10)); print &ui_table_row($text{'nslice_end'}, - &ui_textbox("end", $end, 10)); - +&ui_textbox("end", $end, 10)); + # Slice type -print &ui_table_row($text{'nslice_type'}, - &ui_select("type", 'a5', - [ sort { $a->[1] cmp $b->[1] } - map { [ $_, &fdisk::tag_name($_) ] } - &fdisk::list_tags() ])); - -# Also create partition? -print &ui_table_row($text{'nslice_makepart'}, - &ui_yesno_radio("makepart", 1)); +if (is_using_gpart()) { + my $scheme = ($disk_structure && $disk_structure->{'scheme'}) ? $disk_structure->{'scheme'} : 'GPT'; + my $default_stype = ($scheme =~ /GPT/i) ? 'freebsd-zfs' : 'freebsd'; + print &ui_table_row($text{'nslice_type'}, + &ui_select("type", $default_stype, + [ list_partition_types($scheme) ])); +} +else { + print &ui_table_row($text{'nslice_type'}, + &ui_select("type", 'a5', + [ sort { $a->[1] cmp $b->[1] } + map { [ $_, &fdisk::tag_name($_) ] } + &fdisk::list_tags() ])); +} +# Also create partition? (only for MBR slices with BSD disklabel support) +if (!$is_gpt) { + print &ui_table_row($text{'slice_add'}, + &ui_yesno_radio("makepart", 1)); +} + print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'create'} ] ]); +print &ui_form_end([ [ undef, $text{'save'} ] ]); +# Existing slices summary +print &ui_hr(); +print &ui_columns_start([$text{'disk_no'}, $text{'disk_type'}, $text{'disk_start'}, $text{'disk_end'}, $text{'disk_size'}], $text{'nslice_existing_header'}); +foreach my $s (sort { $a->{'number'} <=> $b->{'number'} } @{$disk->{'slices'}}) { + my $stype = get_type_description($s->{'type'}) || $s->{'type'}; + my $szb = bytes_from_blocks($s->{'device'}, $s->{'blocks'}); + my $sz = defined $szb ? safe_nice_size($szb) : '-'; + print &ui_columns_row([ + $s->{'number'}, + $stype, + $s->{'startblock'}, + $s->{'startblock'} + $s->{'blocks'} - 1, + $sz, + ]); +} +print &ui_columns_end(); + +# Existing partitions summary +my @parts_rows; +foreach my $s (sort { $a->{'number'} <=> $b->{'number'} } @{$disk->{'slices'}}) { + next unless @{$s->{'parts'}||[]}; + foreach my $p (@{$s->{'parts'}}) { + my $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + my $pb = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + my $psz = defined $pb ? safe_nice_size($pb) : '-'; + push @parts_rows, [ $s->{'number'}, uc($p->{'letter'}), $ptype, $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, $psz ]; + } +} +if (@parts_rows) { + print &ui_columns_start(['Slice', $text{'slice_letter'}, $text{'slice_type'}, $text{'slice_start'}, $text{'slice_end'}, $text{'slice_size'}], $text{'nslice_existing_parts'}); + foreach my $row (@parts_rows) { print &ui_columns_row($row); } + print &ui_columns_end(); +} + &ui_print_footer("edit_disk.cgi?device=$in{'device'}", - $text{'disk_return'}); + $text{'disk_return'}); \ No newline at end of file diff --git a/bsdfdisk/smart.cgi b/bsdfdisk/smart.cgi new file mode 100644 index 000000000..35bb3c0a4 --- /dev/null +++ b/bsdfdisk/smart.cgi @@ -0,0 +1,31 @@ +#!/usr/local/bin/perl +# Show SMART status for a given device +use strict; +use warnings; +no warnings 'redefine'; +no warnings 'uninitialized'; +require './bsdfdisk-lib.pl'; +our (%in, %text, $module_name); +&ReadParse(); + +# Validate device param +$in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ or &error($text{'disk_edevice'} || 'Invalid device'); +$in{'device'} !~ /\.\./ or &error($text{'disk_edevice'} || 'Invalid device'); + +# Check smartctl availability +&has_command('smartctl') or &error($text{'index_ecmd'} ? &text('index_ecmd','smartctl') : 'smartctl not available'); + +my $device = $in{'device'}; +my $dev_html = &html_escape($device); + +&ui_print_header($dev_html, $text{'disk_smart'} || 'SMART Status', ""); + +print "
\n"; +print "

SMART status for $dev_html

\n"; +print "
\n"; +my $cmd = "smartctl -a " . "e_path($device) . " 2>&1"; +my $out = &backquote_command($cmd); +print "
" . &html_escape("Command: $cmd\n\n$out") . "
\n"; +print "
\n"; + +&ui_print_footer("edit_disk.cgi?device=".&urlize($device), $text{'disk_return'});