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 element populated with available devices. +sub partition_select { + my ($name, $value) = @_; + my @opts; + my @disks = list_disks_partitions(); + foreach my $d (@disks) { + foreach my $s (@{ $d->{'slices'} || [] }) { + my $stype = get_type_description($s->{'type'}) || $s->{'type'}; + my $ssize_b = bytes_from_blocks($s->{'device'}, $s->{'blocks'}); + my $ssize = defined $ssize_b ? nice_size($ssize_b) : undef; + my $slabel = $s->{'device'} . (defined $ssize ? " ($stype, $ssize)" : " ($stype)"); + push @opts, [ $s->{'device'}, $slabel ]; + foreach my $p (@{ $s->{'parts'} || [] }) { + my $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + my $psz_b = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + my $psz = defined $psz_b ? nice_size($psz_b) : undef; + my $plabel = $p->{'device'} . (defined $psz ? " ($ptype, $psz)" : " ($ptype)"); + push @opts, [ $p->{'device'}, $plabel ]; + } + } + } + # Sort options by device name for consistency + @opts = sort { $a->[0] cmp $b->[0] } @opts; + $value ||= ($opts[0] ? $opts[0]->[0] : undef); + return ui_select($name, $value, \@opts, 1, 0, 0, 0); +} + +# Return a short human-readable description for a given device +# Example: "FreeBSD ZFS, 39G" +sub partition_description { + my ($device) = @_; + return undef unless $device; + my ($ptype, $blocks); + my @disks = list_disks_partitions(); + foreach my $d (@disks) { + foreach my $s (@{ $d->{'slices'} || [] }) { + if ($s->{'device'} && $s->{'device'} eq $device) { + $ptype = get_type_description($s->{'type'}) || $s->{'type'}; + $blocks = $s->{'blocks'}; last; + } + foreach my $p (@{ $s->{'parts'} || [] }) { + if ($p->{'device'} && $p->{'device'} eq $device) { + $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + $blocks = $p->{'blocks'}; last; + } + } + } + } + return undef unless defined $ptype; + my $bytes = bytes_from_blocks($device, $blocks); + my $sz = defined $bytes ? nice_size($bytes) : '-'; + return "$ptype, $sz"; +} + +1; \ No newline at end of file diff --git a/bsdfdisk/create_part.cgi b/bsdfdisk/create_part.cgi index 6b91d2d13..7a8b3bef7 100755 --- a/bsdfdisk/create_part.cgi +++ b/bsdfdisk/create_part.cgi @@ -19,8 +19,10 @@ $slice || &error($text{'slice_egone'}); # Validate inputs, starting with slice number my $part = { }; -$in{'letter'} =~ /^[a-d]$/i || &error($text{'npart_eletter'}); +$in{'letter'} =~ /^[a-h]$/i || &error($text{'npart_eletter'}); $in{'letter'} = lc($in{'letter'}); +# Partition 'c' is reserved in BSD disklabels (represents the whole slice) +$in{'letter'} ne 'c' || &error($text{'npart_ereserved'}); my ($clash) = grep { $_->{'letter'} eq $in{'letter'} } @{$slice->{'parts'}}; $clash && &error(&text('npart_eclash', $in{'letter'})); $part->{'letter'} = $in{'letter'}; @@ -30,7 +32,7 @@ $in{'start'} =~ /^\d+$/ || &error($text{'nslice_estart'}); $in{'end'} =~ /^\d+$/ || &error($text{'nslice_eend'}); $in{'start'} < $in{'end'} || &error($text{'npart_erange'}); $part->{'startblock'} = $in{'start'}; -$part->{'blocks'} = $in{'end'} - $in{'start'}; +$part->{'blocks'} = $in{'end'} - $in{'start'} + 1; # Slice type $part->{'type'} = $in{'type'}; @@ -39,7 +41,8 @@ $part->{'type'} = $in{'type'}; &ui_print_header($slice->{'desc'}, $text{'npart_title'}, ""); print &text('npart_creating', $in{'letter'}, $slice->{'desc'}),"\n"; -my $err = &save_partition($disk, $slice, $part); +# Actually create the partition inside the slice (initialize BSD label if needed) +my $err = &create_partition($disk, $slice, $part); if ($err) { print &text('npart_failed', $err),"\n"; } diff --git a/bsdfdisk/create_slice.cgi b/bsdfdisk/create_slice.cgi index b9e7a1961..e8480f3a3 100755 --- a/bsdfdisk/create_slice.cgi +++ b/bsdfdisk/create_slice.cgi @@ -1,65 +1,109 @@ #!/usr/local/bin/perl -# Actually create a new slice - use strict; use warnings; no warnings 'redefine'; -no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); -&ReadParse(); -&error_setup($text{'nslice_err'}); - -# Get the disk -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); - +ReadParse(); +error_setup($text{'nslice_err'}); +# Get the disk using first() for an early exit on match +my @disks = list_disks_partitions(); +my $disk; +foreach my $d (@disks) { + if ($d->{'device'} eq $in{'device'}) { + $disk = $d; + last; + } +} +# Validate device parameter to prevent path traversal and command injection +$disk or error($text{'disk_egone'}); +# Prefer GPART total blocks for bounds +(my $base_dev = $in{'device'}) =~ s{^/dev/}{}; +my $ds = get_disk_structure($base_dev); +my $disk_blocks = ($ds && $ds->{'total_blocks'}) ? $ds->{'total_blocks'} : ($disk->{'blocks'} || 0); # Validate inputs, starting with slice number -my $slice = { }; -$in{'number'} =~ /^\d+$/ || &error($text{'nslice_enumber'}); -my ($clash) = grep { $_->{'number'} == $in{'number'} } @{$disk->{'slices'}}; -$clash && &error(&text('nslice_eclash', $in{'number'})); +my $slice = {}; +$in{'number'} =~ /^\d+$/ or error($text{'nslice_enumber'}); +# Check for clash using first() with a loop exiting on first match +my $clash; +foreach my $s (@{$disk->{'slices'}}) { + if ($s->{'number'} == $in{'number'}) { + $clash = $s; + last; + } +} $slice->{'number'} = $in{'number'}; - # Start and end blocks -$in{'start'} =~ /^\d+$/ || &error($text{'nslice_estart'}); -$in{'end'} =~ /^\d+$/ || &error($text{'nslice_eend'}); -$in{'start'} < $in{'end'} || &error($text{'nslice_erange'}); +$in{'start'} =~ /^\d+$/ or error($text{'nslice_estart'}); +$in{'end'} =~ /^\d+$/ or error($text{'nslice_eend'}); +($in{'start'} < $in{'end'}) or error($text{'nslice_erange'}); +# total_blocks is the block *after* the last valid block, so end must be < total_blocks +($in{'end'} < $disk_blocks) or error(text('nslice_emax', $disk_blocks - 1)); + +# Ensure the new slice does not overlap existing slices +foreach my $s (@{ $disk->{'slices'} }) { + my $s_start = $s->{'startblock'}; + my $s_end = $s->{'startblock'} + $s->{'blocks'} - 1; + if (!($in{'end'} < $s_start || $in{'start'} > $s_end)) { + error("Requested slice range overlaps with existing slice #".$s->{'number'}); + } +} + $slice->{'startblock'} = $in{'start'}; -$slice->{'blocks'} = $in{'end'} - $in{'start'}; - -# Slice type +$slice->{'blocks'} = $in{'end'} - $in{'start'} + 1; + # Slice type +$in{'type'} =~ /^[a-zA-Z0-9_-]+$/ or error($text{'nslice_etype'}); +length($in{'type'}) <= 20 or error($text{'nslice_etype'}); $slice->{'type'} = $in{'type'}; - # Do the creation -&ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); - -print &text('nslice_creating', $in{'number'}, $disk->{'desc'}),"\n"; -my $err = &create_slice($disk, $slice); +ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); +print text('nslice_creating', $in{'number'}, $disk->{'desc'}), "\n"; +my $err = create_slice($disk, $slice); if ($err) { - print &text('nslice_failed', $err),"\n"; - } -else { - print &text('nslice_done'),"\n"; - } - + print text('nslice_failed', $err), "\n"; +} else { + print text('nslice_done'), "\n"; + # Auto-label the new partition provider with its name if scheme is GPT or BSD + my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + if ($ds && $ds->{'scheme'}) { + # Determine provider and label text + my $label_text = slice_name($slice); # e.g., da8s2 or da0p2 + if ($ds->{'scheme'} =~ /GPT/i) { + my $idx = $slice->{'number'}; + if ($idx) { + my $cmd2 = "gpart modify -i $idx -l " . quote_path($label_text) . " $base"; + my $out2 = `$cmd2 2>&1`; + # If it fails, ignore silently + } + } else { + # On MBR, if BSD label exists we can set label once created; ignore for now + } + } +} if (!$err && $in{'makepart'}) { - # Also create a partition - print &text('nslice_parting', $in{'number'}, $disk->{'desc'}),"\n"; - my $err = &initialize_slice($disk, $slice); - if ($err) { - print &text('nslice_pfailed', $err),"\n"; - } - else { - print &text('nslice_pdone'),"\n"; - } - } - + # Also create a partition (initialize slice label) + my $part_err = initialize_slice($disk, $slice); + if ($part_err) { + print text('nslice_pfailed', $part_err), "\n"; + } else { + print text('nslice_pdone'), "\n"; + } +} if (!$err) { - &webmin_log("create", "slice", $slice->{'device'}, $slice); - } - -&ui_print_footer("edit_disk.cgi?device=$in{'device'}", - $text{'disk_return'}); - + # Auto-label GPT partitions with their device name (e.g., da8p2) + my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + if ($ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i) { + my $slice_devname = $slice->{'device'}; + $slice_devname =~ s{^/dev/}{}; # e.g., da8p2 + my $idx = $slice->{'number'}; + if ($idx && $slice_devname) { + my $label_cmd = "gpart modify -i $idx -l " . quote_path($slice_devname) . " $base 2>&1"; + my $label_out = `$label_cmd`; + # Ignore errors - labeling is optional + } + } + webmin_log("create", "slice", $slice->{'device'}, $slice); +} +ui_print_footer("edit_disk.cgi?device=$in{'device'}", $text{'disk_return'}); \ No newline at end of file diff --git a/bsdfdisk/edit_disk.cgi b/bsdfdisk/edit_disk.cgi index 66a94458b..8a7b0152c 100755 --- a/bsdfdisk/edit_disk.cgi +++ b/bsdfdisk/edit_disk.cgi @@ -1,6 +1,5 @@ #!/usr/local/bin/perl # Show details of a disk, and slices on it - use strict; use warnings; no warnings 'redefine'; @@ -8,93 +7,334 @@ no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); &ReadParse(); -my $extwidth = 300; - +my $extwidth = 100; # Get the disk my @disks = &list_disks_partitions(); my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; $disk || &error($text{'disk_egone'}); - +# Cache commonly used values +my $device = $disk->{'device'}; +my $device_url = &urlize($device); +my $desc = $disk->{'desc'}; +# Prefer total blocks from gpart header when available +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = &get_disk_structure($base_device); +my $disk_blocks = ($disk_structure && $disk_structure->{'total_blocks'}) ? $disk_structure->{'total_blocks'} : ($disk->{'blocks'} || 1000000); +# Precompute a scale factor for extent image widths +my $scale = $extwidth / ($disk_blocks || 1); &ui_print_header($disk->{'desc'}, $text{'disk_title'}, ""); - -# Show disk details -my @info = ( ); -push(@info, &text('disk_dsize', &nice_size($disk->{'size'}))); +# Debug toggle bar +print ""; +if ($in{'debug'}) { + print " $text{'disk_hide_debug'}"; +} else { + print " $text{'disk_show_debug'}"; +} +print ""; +# Get detailed disk information from geom and disk structure from gpart show (cache disk_structure entries) +my $geom_info = &get_detailed_disk_info($device); +my $entries = $disk_structure && $disk_structure->{'entries'} ? $disk_structure->{'entries'} : []; +print &ui_table_start($text{'disk_details'}, "width=100%", 2); +# Prefer mediasize (bytes) for accurate size; fallback to stat-based size +my $disk_bytes = ($disk_structure && $disk_structure->{'mediasize'}) ? $disk_structure->{'mediasize'} : $disk->{'size'}; +print &ui_table_row($text{'disk_dsize'}, &safe_nice_size($disk_bytes)); if ($disk->{'model'}) { - push(@info, &text('disk_model', $disk->{'model'})); + print &ui_table_row($text{'disk_model'}, $disk->{'model'}); +} +print &ui_table_row($text{'disk_device'}, "$disk->{'device'}"); +# Get disk scheme +print &ui_table_row($text{'disk_scheme'}, $disk_structure ? $disk_structure->{'scheme'} : $text{'disk_unknown'}); +# GEOM details +if ($geom_info) { + print &ui_table_hr(); + print &ui_table_row($text{'disk_geom_header'}, "$text{'disk_geom_details'}", 2); + if ($geom_info->{'mediasize'}) { + print &ui_table_row($text{'disk_mediasize'}, $geom_info->{'mediasize'}); + } + if ($geom_info->{'sectorsize'}) { + print &ui_table_row($text{'disk_sectorsize'}, $geom_info->{'sectorsize'} . " " . $text{'disk_bytes'}); + } + if ($geom_info->{'stripesize'}) { + print &ui_table_row($text{'disk_stripesize'}, $geom_info->{'stripesize'} . " " . $text{'disk_bytes'}); + } + if ($geom_info->{'stripeoffset'}) { + print &ui_table_row($text{'disk_stripeoffset'}, $geom_info->{'stripeoffset'} . " " . $text{'disk_bytes'}); + } + if ($geom_info->{'mode'}) { + print &ui_table_row($text{'disk_mode'}, $geom_info->{'mode'}); + } + if ($geom_info->{'rotationrate'}) { + if ($geom_info->{'rotationrate'} eq "0") { + print &ui_table_row($text{'disk_rotationrate'}, $text{'disk_ssd'}); + } else { + print &ui_table_row($text{'disk_rotationrate'}, $geom_info->{'rotationrate'} . " " . $text{'disk_rpm'}); } -push(@info, &text('disk_cylinders', $disk->{'cylinders'})); -push(@info, &text('disk_blocks', $disk->{'blocks'})); -push(@info, &text('disk_device', "$disk->{'device'}")); -print &ui_links_row(\@info),"\n"; + } + if ($geom_info->{'ident'}) { + print &ui_table_row($text{'disk_ident'}, $geom_info->{'ident'}); + } + if ($geom_info->{'lunid'}) { + print &ui_table_row($text{'disk_lunid'}, $geom_info->{'lunid'}); + } + if ($geom_info->{'descr'}) { + print &ui_table_row($text{'disk_descr'}, $geom_info->{'descr'}); + } +} +# Advanced information (cylinders, blocks) +print &ui_table_hr(); +print &ui_table_row($text{'disk_advanced_header'}, "$text{'disk_advanced_details'}", 2); +if ($disk->{'cylinders'}) { + print &ui_table_row($text{'disk_cylinders'}, $disk->{'cylinders'}); +} +print &ui_table_row($text{'disk_blocks'}, $disk->{'blocks'}); +print &ui_table_end(); +# Debug: print raw outputs if debug mode is enabled +if ($in{'debug'}) { + print ""; + # Debug: gpart show output + my $cmd = "gpart show -l $base_device 2>&1"; + my $out = &backquote_command($cmd); + print ""; + print "$text{'disk_debug_gpart'}"; + print ""; + print "Command: $cmd\nOutput:\n$out\n"; + print ""; + + # Debug: disk structure + print ""; + print "$text{'disk_debug_structure'}"; + print ""; + print "Disk Structure:\n"; + foreach my $key (sort keys %$disk_structure) { + if ($key eq 'entries') { + print "entries: [\n"; + foreach my $entry (@{$disk_structure->{'entries'}}) { + print " {\n"; + foreach my $k (sort keys %$entry) { + print " $k: $entry->{$k}\n"; + } + print " },\n"; + } + print "]\n"; + } else { + print "$key: $disk_structure->{$key}\n"; + } + } + print ""; + print ""; + + # Debug: Raw GEOM output + print ""; + print "$text{'disk_debug_geom'}"; + print ""; + print "Raw GEOM output:\n"; + print &html_escape(&backquote_command("geom disk list " . "e_path($device) . " 2>/dev/null")); + print ""; + print ""; + print ""; +} +# Build partition details from disk_structure (no separate gpart list call) +my %part_details = (); +if ($disk_structure && $disk_structure->{'partitions'}) { + %part_details = %{ $disk_structure->{'partitions'} }; +} +# Ensure we have names/labels for any entries missing from partitions map +if ($disk_structure && $disk_structure->{'entries'}) { + foreach my $entry (@{$disk_structure->{'entries'}}) { + next unless ($entry->{'type'} eq 'partition' && $entry->{'index'}); + my $part_num = $entry->{'index'}; + $part_details{$part_num} ||= {}; + $part_details{$part_num}->{'name'} ||= $base_device . (($disk_structure->{'scheme'} eq 'GPT') ? "p$part_num" : "s$part_num"); + if ($entry->{'label'} && $entry->{'label'} ne '(null)') { + $part_details{$part_num}->{'label'} ||= $entry->{'label'}; + } + $part_details{$part_num}->{'type'} ||= $entry->{'part_type'} || 'unknown'; + } +} +# Build ZFS devices cache +my ($zfs_pools, $zfs_devices) = &build_zfs_devices_cache(); +# Debug ZFS pools if debug mode is enabled +if ($in{'debug'}) { + print ""; + print ""; + print "$text{'disk_debug_zfs'}"; + print ""; + print ""; + my $cmd = "zpool status 2>&1"; + my $out = &backquote_command($cmd); + print "Command: $cmd\nOutput:\n$out\n"; + print ""; + print ""; + print ""; +} +# Debug: Print partition details mapping if debug enabled +if ($in{'debug'}) { + print ""; + print ""; + print "$text{'disk_debug_part_details'}"; + print ""; + print "Partition Details Mapping:\n"; + foreach my $pnum (sort { $a <=> $b } keys %part_details) { + print " $pnum: {\n"; + foreach my $k (sort keys %{$part_details{$pnum}}) { + print " $k: $part_details{$pnum}->{$k}\n"; + } + print " },\n"; + } + print ""; + print ""; + print ""; +} +# Get sector size +my $sectorsize = $disk_structure->{'sectorsize'} || &get_disk_sectorsize($device) || 512; +my $sectorsize_text = $sectorsize ? "$sectorsize" : "512"; # Show partitions table -my @links = ( "".$text{'disk_add'}."" ); -if (@{$disk->{'slices'}}) { - print &ui_links_row(\@links); - print &ui_columns_start([ - $text{'disk_no'}, - $text{'disk_type'}, - $text{'disk_extent'}, - $text{'disk_size'}, - $text{'disk_start'}, - $text{'disk_end'}, - $text{'disk_use'}, - ]); - foreach my $p (@{$disk->{'slices'}}) { - # Create images for the extent - my $ext = ""; - $ext .= sprintf "", - $extwidth*($p->{'startblock'} - 1) / - $disk->{'blocks'}; - $ext .= sprintf "", - $p->{'extended'} ? "ext" : "use", - $extwidth*($p->{'blocks'}) / - $disk->{'blocks'}; - $ext .= sprintf "", - $extwidth*($disk->{'blocks'} - $p->{'startblock'} - - $p->{'blocks'}) / $disk->{'blocks'}; - - # Work out use - my @st = &fdisk::device_status($p->{'device'}); - my $use = &fdisk::device_status_link(@st); - my $n = scalar(@{$p->{'parts'}}); - - # Add row for the slice - my $url = "edit_slice.cgi?device=".&urlize($disk->{'device'}). - "&slice=".$p->{'number'}; - my $nlink = "$p->{'number'}"; - $nlink = "$nlink" if ($p->{'active'}); - print &ui_columns_row([ - $nlink, - "".&fdisk::tag_name($p->{'type'})."", - $ext, - &nice_size($p->{'size'}), - $p->{'startblock'}, - $p->{'startblock'} + $p->{'blocks'} - 1, - $use ? $use : - $n ? &text('disk_scount', $n) : "", - ]); - } - print &ui_columns_end(); - } -else { - print "$text{'disk_none'}\n"; - } +my @links = ( "".$text{'disk_add'}."" ); +if (@$entries) { + print &ui_links_row(\@links); + print &ui_columns_start([ + $text{'disk_no'}, # Row number + $text{'disk_partno'}, # Part. No. + $text{'disk_partname'}, # Part. Name + $text{'disk_partlabel'}, # Part. Label + $text{'disk_subpart'}, # Sub-part. + $text{'disk_extent'}, # Extent + $text{'disk_start'}, # Startblock + $text{'disk_end'}, # Endblock + $text{'disk_size'}, # Size + $text{'disk_format'}, # Format type + $text{'disk_use'}, # Used by + $text{'disk_role'}, # Role Type + ]); + my $row_number = 1; + foreach my $entry (@$entries) { + my @cols = (); + push(@cols, $row_number++); + if ($entry->{'type'} eq 'free') { + my $start = $entry->{'start'}; + my $end = $entry->{'start'} + $entry->{'size'} - 1; + my $create_url = "slice_form.cgi?device=$device_url&new=1&start=$start&end=$end"; + push(@cols, "".$text{'disk_free'}.""); + push(@cols, "-"); + push(@cols, "-"); + push(@cols, "-"); + my $ext = ""; + $ext .= sprintf "", $scale * ($entry->{'start'} - 1); + $ext .= sprintf "", $scale * ($entry->{'size'}); + $ext .= sprintf "", $scale * ($disk_blocks - $entry->{'start'} - $entry->{'size'}); + push(@cols, $ext); + push(@cols, $start); + push(@cols, $end); + push(@cols, $entry->{'size_human'}); + push(@cols, $text{'disk_free_space'}); + push(@cols, $text{'disk_available'}); + push(@cols, "-"); + } else { + my $part_num = $entry->{'index'}; + my $ext = ""; + $ext .= sprintf "", $scale * ($entry->{'start'} - 1); + $ext .= sprintf "", $scale * ($entry->{'size'}); + $ext .= sprintf "", $scale * ($disk_blocks - $entry->{'start'} - $entry->{'size'}); + my $url = "edit_slice.cgi?device=$device_url&slice=".&urlize($part_num); + push(@cols, "".&html_escape($part_num).""); + + my $part_info = $part_details{$part_num}; + my $part_name = $part_info ? $part_info->{'name'} : "-"; + push(@cols, $part_name); + my $part_label = $part_info ? $part_info->{'label'} : ($entry->{'label'} eq "(null)" ? "-" : $entry->{'label'}); + push(@cols, $part_label); + + # Find sub-partitions if available + my ($slice) = grep { $_->{'number'} eq $part_num } @{$disk->{'slices'} || []}; + my $sub_part_info = ($slice && scalar(@{$slice->{'parts'}||[]}) > 0) ? + join(", ", map { $_->{'letter'} } @{$slice->{'parts'}}) : "-"; + push(@cols, $sub_part_info); + + push(@cols, $ext); + push(@cols, $entry->{'start'}); + push(@cols, $entry->{'start'} + $entry->{'size'} - 1); + push(@cols, $entry->{'size_human'}); + + # Classify format/use/role via library helper + my ($format_type, $usage, $role) = classify_partition_row( + base_device => $base_device, + scheme => ($disk_structure->{'scheme'} || ''), + part_num => $part_num, + part_name => $part_name, + part_label => $part_label, + entry_part_type => ($part_info ? $part_info->{'type'} : $entry->{'part_type'}), + entry_rawtype => ($part_info ? $part_info->{'rawtype'} : undef), + size_human => $entry->{'size_human'}, + size_blocks => $entry->{'size'}, + zfs_devices => $zfs_devices, + ); + push(@cols, $format_type || '-'); + push(@cols, $usage || $text{'part_nouse'}); + push(@cols, $role || '-'); + } + print &ui_columns_row(\@cols); + } + print &ui_columns_end(); +} else { + if (@{$disk->{'slices'}||[]}) { + print &ui_links_row(\@links); + print &ui_columns_start([ + $text{'disk_no'}, + $text{'disk_type'}, + $text{'disk_extent'}, + $text{'disk_start'}, + $text{'disk_end'}, + $text{'disk_use'}, + ]); + foreach my $s (@{$disk->{'slices'}}) { + my @cols = (); + my $ext = ""; + $ext .= sprintf "", $scale * ($s->{'startblock'} - 1); + $ext .= sprintf "", ($s->{'extended'} ? "ext" : "use"), $scale * ($s->{'blocks'}); + $ext .= sprintf "", $scale * ($disk_blocks - $s->{'startblock'} - $s->{'blocks'}); + my $url = "edit_slice.cgi?device=$device_url&slice=".&urlize($s->{'number'}); + push(@cols, "".&html_escape($s->{'number'}).""); + push(@cols, &get_type_description($s->{'type'}) || $s->{'type'}); + push(@cols, $ext); + push(@cols, $s->{'startblock'}); + push(@cols, $s->{'startblock'} + $s->{'blocks'} - 1); + my @st = &fdisk::device_status($s->{'device'}); + my $use = &fdisk::device_status_link(@st); + push(@cols, $use || $text{'part_nouse'}); + print &ui_columns_row(\@cols); + } + print &ui_columns_end(); + } else { + print "$text{'disk_none'}\n"; + } +} print &ui_links_row(\@links); - -print &ui_hr(); -print &ui_buttons_start(); - -if (&foreign_installed("smart-status")) { - print &ui_buttons_row( - "../smart-status/index.cgi", - $text{'disk_smart'}, - $text{'disk_smartdesc'}, - &ui_hidden("drive", $disk->{'device'}.":")); - } - -print &ui_buttons_end(); - -&ui_print_footer("", $text{'index_return'}); +# Show SMART status link if available +if (&has_command("smartctl")) { + print &ui_hr(); + print &ui_buttons_start(); + print &ui_buttons_row("smart.cgi", $text{'disk_smart'}, $text{'disk_smartdesc'}, &ui_hidden("device", $device)); + print &ui_buttons_end(); +} +# Debug: ZFS cache detail +if ($in{'debug'}) { + print ""; + print ""; + print "$text{'disk_debug_zfs_cache'}"; + print ""; + print "Pools: " . join(", ", keys %$zfs_pools) . "\n\nDevices:\n"; + foreach my $device_id (sort keys %$zfs_devices) { + next if $device_id =~ /^_debug_/; + my $device_info = $zfs_devices->{$device_id}; + print "$device_id => Pool: $device_info->{'pool'}, Type: $device_info->{'vdev_type'}, Mirrored: " . + ($device_info->{'is_mirrored'} ? "Yes" : "No") . ", RAIDZ: " . + ($device_info->{'is_raidz'} ? "Yes (Level: $device_info->{'raidz_level'})" : "No") . + ", Single: " . ($device_info->{'is_single'} ? "Yes" : "No") . + ", Striped: " . ($device_info->{'is_striped'} ? "Yes" : "No") . "\n"; + } + print ""; + print ""; + print ""; +} +&ui_print_footer("", $text{'disk_return'}); diff --git a/bsdfdisk/edit_part.cgi b/bsdfdisk/edit_part.cgi index 87ff74a0b..67d15f90c 100755 --- a/bsdfdisk/edit_part.cgi +++ b/bsdfdisk/edit_part.cgi @@ -1,87 +1,126 @@ #!/usr/local/bin/perl -# Show details of a partition, with buttons to create a filesystem - use strict; use warnings; -no warnings 'redefine'; -no warnings 'uninitialized'; -require './bsdfdisk-lib.pl'; -our (%in, %text, $module_name); -&ReadParse(); - -# Get the disk and slice -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; -$slice || &error($text{'slice_egone'}); -my ($part) = grep { $_->{'letter'} eq $in{'part'} } @{$slice->{'parts'}}; -$part || &error($text{'part_egone'}); - -&ui_print_header($part->{'desc'}, $text{'part_title'}, ""); +# Load required libraries +require "./bsdfdisk-lib.pl"; +our ( %in, %text, $module_name ); +ReadParse(); +# Cache input parameters to avoid repeated hash lookups +my $device = $in{'device'}; +my $slice_num = $in{'slice'}; +my $part_letter = $in{'part'}; +# Get the disk and slice using first() to stop at the first matching element +my @disks = list_disks_partitions(); +$in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ or error($text{'disk_edevice'}); +$in{'device'} !~ /\.\./ or error($text{'disk_edevice'}); +my $disk; +foreach my $d (@disks) { + if ($d->{'device'} eq $device) { + $disk = $d; + last; + } +} +$disk or error($text{'disk_egone'}); +my $slice; +foreach my $s (@{$disk->{'slices'}}) { + if ($s->{'number'} eq $slice_num) { + $slice = $s; + last; + } +} +$slice or error($text{'slice_egone'}); +my $part; +foreach my $p (@{$slice->{'parts'}}) { + if ($p->{'letter'} eq $part_letter) { $part = $p; last; } +} +$part or error($text{'part_egone'}); +ui_print_header($part->{'desc'}, $text{'part_title'}, ""); +# Check if this is a boot partition +my $is_boot = is_boot_partition($part); +if ($is_boot) { + print ui_alert_box($text{'part_bootdesc'}, 'info'); +} # Show current details -my @st = &fdisk::device_status($part->{'device'}); -my $use = &fdisk::device_status_link(@st); -my $canedit = !@st || !$st[2]; -my $hiddens = &ui_hidden("device", $in{'device'})."\n". - &ui_hidden("slice", $in{'slice'})."\n". - &ui_hidden("part", $in{'part'})."\n"; +my $zfs_info = get_all_zfs_info(); +my @st = fdisk::device_status($part->{'device'}); +# calculate $use from either ZFS info or from a status link +my $device_path = $part->{'device'}; +my $use = $zfs_info->{ $device_path } || fdisk::device_status_link(@st); +my $canedit = (!@st && !$zfs_info->{ $device_path } && !$is_boot); +# Prepare hidden fields once +my $hiddens = ui_hidden("device", $device) . "\n" . + ui_hidden("slice", $slice_num) . "\n" . + ui_hidden("part", $part_letter) . "\n"; if ($canedit) { - print &ui_form_start("save_part.cgi", "post"); - print $hiddens; - } -print &ui_table_start($text{'part_header'}, undef, 2); - -print &ui_table_row($text{'part_device'}, - "$part->{'device'}"); - -print &ui_table_row($text{'part_size'}, - &nice_size($part->{'size'})); - -print &ui_table_row($text{'part_start'}, - $part->{'startblock'}); - -print &ui_table_row($text{'part_end'}, - $part->{'startblock'} + $part->{'blocks'} - 1); - + print ui_form_start("save_part.cgi", "post"), $hiddens; +} +print ui_table_start($text{'part_header'}, undef, 2); +print ui_table_row($text{'part_device'}, "$part->{'device'}"); +my $part_bytes = bytes_from_blocks($part->{'device'}, $part->{'blocks'}); +print ui_table_row($text{'part_size'}, $part_bytes ? safe_nice_size($part_bytes) : '-'); +print ui_table_row($text{'part_start'}, $part->{'startblock'}); +print ui_table_row($text{'part_end'}, $part->{'startblock'} + $part->{'blocks'} - 1); +my $disk_geom = get_detailed_disk_info($disk->{'device'}); +my $stripesize = ($disk_geom && $disk_geom->{'stripesize'}) ? $disk_geom->{'stripesize'} : '-'; +print ui_table_row($text{'disk_stripesize'}, $stripesize); if ($canedit) { - print &ui_table_row($text{'part_type'}, - &ui_select("type", $part->{'type'}, - [ &list_partition_types() ], 1, 0, 1)); - } -else { - print &ui_table_row($text{'part_type'}, - $part->{'type'}); - } - -print &ui_table_row($text{'part_use'}, - !@st ? $text{'part_nouse'} : - $st[2] ? &text('part_inuse', $use) : - &text('part_foruse', $use)); - -print &ui_table_end(); + # BSD disklabel partitions only support FreeBSD types + print ui_table_row($text{'part_type'}, + ui_select("type", $part->{'type'}, [ list_partition_types('BSD') ], 1, 0, 1)); +} else { + print ui_table_row($text{'part_type'}, get_format_type($part)); +} +my $use_text = ((!@st && !$zfs_info->{ $part->{'device'} }) + ? $text{'part_nouse'} + : (($st[2] || $zfs_info->{ $part->{'device'} }) + ? text('part_inuse', $use) + : text('part_foruse', $use))); +print ui_table_row($text{'part_use'}, $use_text); +# Add a row for the partition role +print ui_table_row($text{'part_role'}, get_partition_role($part)); +print ui_table_end(); if ($canedit) { - print &ui_form_end([ [ undef, $text{'save'} ] ]); - } + print ui_form_end([[ undef, $text{'save'} ]]); +} -# Show newfs and mount buttons +# Existing partitions on this slice +if (@{ $slice->{'parts'} || [] }) { + my $zfs = get_all_zfs_info(); + print ui_hr(); + print ui_columns_start([ + $text{'slice_letter'}, $text{'slice_type'}, $text{'slice_start'}, $text{'slice_end'}, $text{'slice_size'}, $text{'slice_use'}, $text{'slice_role'} + ], $text{'epart_existing'}); + foreach my $p (sort { $a->{'startblock'} <=> $b->{'startblock'} } @{ $slice->{'parts'} }) { + my $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + my @stp = fdisk::device_status($p->{'device'}); + my $usep = $zfs->{$p->{'device'}} || fdisk::device_status_link(@stp) || $text{'part_nouse'}; + my $rolep = get_partition_role($p); + my $pb2 = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + print ui_columns_row([ + uc($p->{'letter'}), $ptype, $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, ($pb2 ? safe_nice_size($pb2) : '-'), $usep, $rolep + ]); + } + print ui_columns_end(); +} + +# Show newfs and mount buttons if editing is allowed if ($canedit) { - print &ui_hr(); - - print &ui_buttons_start(); - - &show_filesystem_buttons($hiddens, \@st, $part); - - print &ui_buttons_row( - "delete_part.cgi", $text{'part_delete'}, - $text{'part_deletedesc'}, $hiddens); - - print &ui_buttons_end(); - } -else { - print "$text{'part_cannotedit'}\n"; - } - -&ui_print_footer("edit_slice.cgi?device=$in{'device'}&slice=$in{'slice'}", - $text{'slice_return'}); + print ui_hr(); + print ui_buttons_start(); + show_filesystem_buttons($hiddens, \@st, $part); + print ui_buttons_row("delete_part.cgi", $text{'part_delete'}, $text{'part_deletedesc'}, $hiddens); + print ui_buttons_end(); +} else { + print ($is_boot) ? "$text{'part_bootcannotedit'}\n" + : "$text{'part_cannotedit'}\n"; +} +# SMART button (physical device) +if (&has_command("smartctl")) { + print ui_hr(); + print ui_buttons_start(); + print ui_buttons_row("smart.cgi", $text{'disk_smart'}, $text{'disk_smartdesc'}, + ui_hidden("device", $disk->{'device'})); + print ui_buttons_end(); +} +ui_print_footer("edit_slice.cgi?device=$device&slice=$slice_num", $text{'slice_return'}); \ No newline at end of file diff --git a/bsdfdisk/edit_slice.cgi b/bsdfdisk/edit_slice.cgi index 3a901154c..931695384 100755 --- a/bsdfdisk/edit_slice.cgi +++ b/bsdfdisk/edit_slice.cgi @@ -1,146 +1,178 @@ #!/usr/local/bin/perl -# Show details of a slice, and partitions on it - use strict; use warnings; no warnings 'redefine'; no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); -&ReadParse(); +ReadParse(); my $extwidth = 300; - # Get the disk and slice -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; -$slice || &error($text{'slice_egone'}); - -&ui_print_header($slice->{'desc'}, $text{'slice_title'}, ""); - +my @disks = list_disks_partitions(); +my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks or error($text{'disk_egone'}); +my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}} or error($text{'slice_egone'}); +ui_print_header($slice->{'desc'}, $text{'slice_title'}, ""); # Show slice details -my @st = &fdisk::device_status($slice->{'device'}); -my $use = &fdisk::device_status_link(@st); -my $canedit = !@st || !$st[2]; -my $hiddens = &ui_hidden("device", $in{'device'})."\n". - &ui_hidden("slice", $in{'slice'})."\n"; -print &ui_form_start("save_slice.cgi"); +my $zfs_info = get_all_zfs_info(); +my ($zfs_pools, $zfs_devices) = build_zfs_devices_cache(); +# Cache slice device status +my @slice_status = fdisk::device_status($slice->{'device'}); +my $slice_use = $zfs_info->{$slice->{'device'}} ? $zfs_info->{$slice->{'device'}} : fdisk::device_status_link(@slice_status); +my $canedit = (! @slice_status || !$slice_status[2]); +# Prepare hidden fields +my $hiddens = ui_hidden("device", $in{'device'}) . "\n" . ui_hidden("slice", $in{'slice'}) . "\n"; +# Derive disk scheme for classifier +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = get_disk_structure($base_device); +# Check if this is a boot slice +my $is_boot = is_boot_partition($slice); +print ui_alert_box($text{'slice_bootdesc'}, 'info') if $is_boot; +print ui_form_start("save_slice.cgi"); print $hiddens; -print &ui_table_start($text{'slice_header'}, undef, 2); - -print &ui_table_row($text{'part_device'}, - "$slice->{'device'}"); - -print &ui_table_row($text{'slice_ssize'}, - &nice_size($slice->{'size'})); - -print &ui_table_row($text{'slice_sstart'}, - $slice->{'startblock'}); - -print &ui_table_row($text{'slice_send'}, - $slice->{'startblock'} + $slice->{'blocks'} - 1); - -print &ui_table_row($text{'slice_stype'}, - &ui_select("type", $slice->{'type'}, - [ sort { $a->[1] cmp $b->[1] } - map { [ $_, &fdisk::tag_name($_) ] } - &fdisk::list_tags() ])); - -print &ui_table_row($text{'slice_sactive'}, - $slice->{'active'} ? $text{'yes'} : - &ui_yesno_radio("active", $slice->{'active'})); - -print &ui_table_row($text{'slice_suse'}, - !@st ? $text{'part_nouse'} : - $st[2] ? &text('part_inuse', $use) : - &text('part_foruse', $use)); - -print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'save'} ] ]); - -print &ui_hr(); - -# Show partitions table -my @links = ( "".$text{'slice_add'}."" ); -if (@{$slice->{'parts'}}) { - print &ui_links_row(\@links); - print &ui_columns_start([ - $text{'slice_letter'}, - $text{'slice_type'}, - $text{'slice_extent'}, - $text{'slice_size'}, - $text{'slice_start'}, - $text{'slice_end'}, - $text{'slice_use'}, - ]); - foreach my $p (@{$slice->{'parts'}}) { - # Create images for the extent - my $ext = ""; - $ext .= sprintf "", - $extwidth*($p->{'startblock'} - 1) / - $slice->{'blocks'}; - $ext .= sprintf "", - $p->{'extended'} ? "ext" : "use", - $extwidth*($p->{'blocks'}) / - $slice->{'blocks'}; - $ext .= sprintf "", - $extwidth*($slice->{'blocks'} - $p->{'startblock'} - - $p->{'blocks'}) / $slice->{'blocks'}; - - # Work out use - my @st = &fdisk::device_status($p->{'device'}); - my $use = &fdisk::device_status_link(@st); - - # Add row for the partition - my $url = "edit_part.cgi?device=".&urlize($disk->{'device'}). - "&slice=".$slice->{'number'}."&part=".$p->{'letter'}; - print &ui_columns_row([ - "".uc($p->{'letter'})."", - "$p->{'type'}", - $ext, - &nice_size($p->{'size'}), - $p->{'startblock'}, - $p->{'startblock'} + $p->{'blocks'} - 1, - $use, - ]); - } - print &ui_columns_end(); - print &ui_links_row(\@links); - } +print ui_table_start($text{'slice_header'}, undef, 2); +print ui_table_row($text{'part_device'}, "$slice->{'device'}"); +my $slice_bytes = bytes_from_blocks($slice->{'device'}, $slice->{'blocks'}); +print ui_table_row($text{'slice_ssize'}, $slice_bytes ? safe_nice_size($slice_bytes) : '-'); +print ui_table_row($text{'slice_sstart'}, $slice->{'startblock'}); +print ui_table_row($text{'slice_send'}, $slice->{'startblock'} + $slice->{'blocks'} - 1); +# Slice type selector (GPT vs legacy) +if (is_using_gpart()) { + my $scheme = ($disk_structure && $disk_structure->{'scheme'}) ? $disk_structure->{'scheme'} : 'GPT'; + my @opts = list_partition_types($scheme); + # Default sensibly per scheme + my $default_type = ($scheme =~ /GPT/i) ? 'freebsd-zfs' : 'freebsd'; + print ui_table_row($text{'slice_stype'}, ui_select("type", $slice->{'type'} || $default_type, \@opts)); +} else { - # No partitions yet - if (@st) { - # And directly in use, so none can be created - print "$text{'slice_none2'}\n"; - } - else { - # Show link to add first partition - print "$text{'slice_none'}\n"; - print &ui_links_row(\@links); - } - } + # Pre-cache tag options for the slice type select (legacy fdisk) + my @tags = fdisk::list_tags(); + my @tag_options = map { [ $_, fdisk::tag_name($_) ] } @tags; + @tag_options = sort { $a->[1] cmp $b->[1] } @tag_options; + print ui_table_row($text{'slice_stype'}, ui_select("type", $slice->{'type'}, \@tag_options)); +} +# Active slice - only applicable for legacy MBR. For GPT/UEFI and for EFI/freebsd-boot types, the active flag is irrelevant. +my $is_gpt = is_using_gpart() && ($disk_structure && $disk_structure->{'scheme'} && $disk_structure->{'scheme'} =~ /GPT/i); +if (!$is_gpt && ($slice->{'type'} !~ /^(?:efi|freebsd-boot)$/i)) { + my $active_default = $slice->{'active'} ? 1 : 0; + print ui_table_row($text{'slice_sactive'}, ui_yesno_radio("active", $active_default)); +} else { + # Do not offer the control; display 'No' since active is not used here + print ui_table_row($text{'slice_sactive'}, $text{'no'}); +} +print ui_table_row($text{'slice_suse'}, + (!$slice_use || $slice_use eq $text{'part_nouse'}) + ? $text{'part_nouse'} + : ($slice_status[2] ? text('part_inuse', $slice_use) : text('part_foruse', $slice_use))); +# Add a row for the slice role +print ui_table_row($text{'slice_role'}, get_partition_role($slice)); +print ui_table_end(); +print ui_form_end([ [ undef, $text{'save'} ] ]); +print ui_hr(); +# Show partitions table (only for MBR slices that support BSD disklabel) +my $can_have_parts = 0; +if (!is_using_gpart()) { + # Legacy MBR with BSD disklabel + $can_have_parts = 1; +} elsif ($disk_structure && $disk_structure->{'scheme'} && $disk_structure->{'scheme'} !~ /GPT/i) { + # MBR-style slice + $can_have_parts = 1; +} +my @links = $can_have_parts ? ( "" . $text{'slice_add'} . "" ) : (); +if (@{$slice->{'parts'}}) { + print ui_links_row(\@links) if @links; + print ui_columns_start([ + $text{'slice_letter'}, + $text{'slice_type'}, + $text{'slice_extent'}, + $text{'slice_size'}, + $text{'slice_start'}, + $text{'slice_end'}, + $text{'disk_stripesize'}, + $text{'slice_use'}, + $text{'slice_role'}, + ]); + + # Pre-calculate scaling factor for the partition extent images + my $scale = $extwidth / $slice->{'blocks'}; + + foreach my $p (@{$slice->{'parts'}}) { + # Create images representing the partition extent + my $gap_before = sprintf("", int($scale * ($p->{'startblock'} - 1))); + my $img_type = $p->{'extended'} ? "ext" : "use"; + my $partition_img = sprintf("", $img_type, int($scale * $p->{'blocks'})); + my $gap_after = sprintf("", int($scale * ($slice->{'blocks'} - $p->{'startblock'} - $p->{'blocks'}))); + my $ext = $gap_before . $partition_img . $gap_after; + + # Cache partition device status information + my @part_status = fdisk::device_status($p->{'device'}); + my $part_use = $zfs_info->{$p->{'device'}} || fdisk::device_status_link(@part_status); + # Prefer GEOM details for stripesize + my $ginfo = get_detailed_disk_info($p->{'device'}); + my $stripesize = ($ginfo && $ginfo->{'stripesize'}) ? $ginfo->{'stripesize'} : '-'; + + # Classify format/use/role via library helper + (my $pn = $p->{'device'}) =~ s{^/dev/}{}; + my ($fmt, $use_txt, $role_txt) = classify_partition_row( + base_device => $base_device, + scheme => ($disk_structure->{'scheme'} || ''), + part_name => $pn, + entry_part_type => $p->{'type'}, + zfs_devices => $zfs_devices, + ); + $use_txt ||= $part_use; + $role_txt ||= get_partition_role($p); + + # Build edit URL + my $url = "edit_part.cgi?device=" . urlize($disk->{'device'}) . "&slice=" . $slice->{'number'} . "&part=" . $p->{'letter'}; + my $psz_b = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + print ui_columns_row([ + "" . uc($p->{'letter'}) . "", + "" . ($fmt || get_format_type($p)) . "", + $ext, + ($psz_b ? safe_nice_size($psz_b) : '-'), + $p->{'startblock'}, + $p->{'startblock'} + $p->{'blocks'} - 1, + $stripesize, + $use_txt, + $role_txt, + ]); + } + print ui_columns_end(); + print ui_links_row(\@links) if @links; +} else { + # GPT partitions do not have sub-partitions + if (!$can_have_parts) { + # No message needed for GPT; partitions are top-level + } + # If slice is in use by a filesystem OR it is a boot slice, do not allow creating partitions + elsif (@slice_status || $zfs_info->{$slice->{'device'}} || $is_boot) { + print "$text{'slice_none2'}\n"; + } else { + print "$text{'slice_none'}\n"; + print ui_links_row(\@links) if @links; + } +} +if ($canedit && !$is_boot) { # Do not allow editing boot slices + print ui_hr(); + print ui_buttons_start(); + if (!@{$slice->{'parts'}}) { + show_filesystem_buttons($hiddens, \@slice_status, $slice); + } + print ui_buttons_row( + 'delete_slice.cgi', + $text{'slice_delete'}, + $text{'slice_deletedesc'}, + ui_hidden("device", $in{'device'}) . "\n" . ui_hidden("slice", $in{'slice'}) + ); + print ui_buttons_end(); +} +# SMART button (physical device) +if (&has_command("smartctl")) { + print ui_hr(); + print ui_buttons_start(); + print ui_buttons_row("smart.cgi", $text{'disk_smart'}, $text{'disk_smartdesc'}, + ui_hidden("device", $disk->{'device'})); + print ui_buttons_end(); +} -if ($canedit) { - print &ui_hr(); - print &ui_buttons_start(); - - if (!@{$slice->{'parts'}}) { - &show_filesystem_buttons($hiddens, \@st, $slice); - } - - # Button to delete slice - print &ui_buttons_row( - 'delete_slice.cgi', - $text{'slice_delete'}, - $text{'slice_deletedesc'}, - &ui_hidden("device", $in{'device'})."\n". - &ui_hidden("slice", $in{'slice'})); - - print &ui_buttons_end(); - } - - -&ui_print_footer("edit_disk.cgi?device=$in{'device'}", - $text{'disk_return'}); +ui_print_footer("edit_disk.cgi?device=$in{'device'}", $text{'disk_return'}); \ No newline at end of file diff --git a/bsdfdisk/fsck.cgi b/bsdfdisk/fsck.cgi index b4f6a8b78..13a1dc737 100755 --- a/bsdfdisk/fsck.cgi +++ b/bsdfdisk/fsck.cgi @@ -27,28 +27,54 @@ else { $object = $slice; } +# Safety checks: do not run fsck on boot partitions or in-use devices +if (is_boot_partition($object)) { + &error($in{'part'} ne '' ? $text{'part_eboot'} : $text{'slice_eboot'}); +} +my @st_obj = &fdisk::device_status($object->{'device'}); +my $use_obj = &fdisk::device_status_link(@st_obj); +if (@st_obj && $st_obj[2]) { + &error(&text('part_esave', $use_obj)); +} + &ui_print_unbuffered_header($object->{'desc'}, $text{'fsck_title'}, ""); -# Do the creation -print &text('fsck_checking', "$object->{'device'}"),"\n"; -print "\n"; -my $cmd = &get_check_filesystem_command($disk, $slice, $part); -&additional_log('exec', undef, $cmd); -my $fh = "CMD"; -&open_execute_command($fh, $cmd, 2); -while(<$fh>) { - print &html_escape($_); - } -close($fh); -print ""; -if ($?) { - print $text{'fsck_failed'},"\n"; - } -else { - print $text{'fsck_done'},"\n"; - } +# If device is ZFS, do not run fsck; show zpool status instead +my $zmap = get_all_zfs_info(); +if ($zmap->{$object->{'device'}}) { + my $pool = $zmap->{$object->{'device'}}; $pool =~ s/^.*?\b([A-Za-z0-9_\-]+)\b.*$/$1/; + print &text('fsck_checking', "$object->{'device'}"),"\n"; + print "\n"; + my $cmd = "zpool status 2>&1"; + &additional_log('exec', undef, $cmd); + print &html_escape(&backquote_command($cmd)); + print ""; + print $text{'fsck_done'},"\n"; +} else { + # Do the creation + print &text('fsck_checking', "$object->{'device'}"),"\n"; + print "\n"; + my $cmd = &get_check_filesystem_command($disk, $slice, $part); + &additional_log('exec', undef, $cmd); + my $fh; + &open_execute_command($fh, $cmd, 2); + if ($fh) { + while (my $line = <$fh>) { + $line =~ s/[^\x09\x0A\x0D\x20-\x7E]//g; + print &html_escape($line); + } + close($fh); + } + print ""; + if ($?) { + print $text{'fsck_failed'},"\n"; + } + else { + print $text{'fsck_done'},"\n"; + } +} &webmin_log("fsck", $in{'part'} ne '' ? "part" : "object", - $object->{'device'}, $object); + $object->{'device'}, $object); if ($in{'part'} ne '') { &ui_print_footer("edit_part.cgi?device=$in{'device'}&". @@ -59,4 +85,4 @@ else { &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". "slice=$in{'slice'}", $text{'slice_return'}); - } + } \ No newline at end of file diff --git a/bsdfdisk/images/free.gif b/bsdfdisk/images/free.gif new file mode 100644 index 000000000..df1df5e0d Binary files /dev/null and b/bsdfdisk/images/free.gif differ diff --git a/bsdfdisk/index.cgi b/bsdfdisk/index.cgi index 589e62ee2..2964b9099 100755 --- a/bsdfdisk/index.cgi +++ b/bsdfdisk/index.cgi @@ -1,41 +1,57 @@ #!/usr/local/bin/perl -# Show a list of disks - use strict; use warnings; -no warnings 'redefine'; -no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, %config, $module_name); -&ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0, - &help_search_link("fdisk", "man")); - -my $err = &check_fdisk(); +# Check prerequisites first +my $err = check_fdisk(); if ($err) { - &ui_print_endpage(&text('index_problem', $err)); - } + ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0); + print "$text{'index_problem'}\n$err\n"; + ui_print_footer("/", $text{'index_return'}); + exit; +} + +# Print header with help link +ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0, + help_search_link("fdisk", "man")); + +# List and sort disks by device name +my @disks = list_disks_partitions(); +@disks = sort { ($a->{'device'}//'') cmp ($b->{'device'}//'') } @disks; -my @disks = &list_disks_partitions(); -@disks = sort { $a->{'device'} cmp $b->{'device'} } @disks; if (@disks) { - print &ui_columns_start([ $text{'index_dname'}, - $text{'index_dsize'}, - $text{'index_dmodel'}, - $text{'index_dparts'} ]); - foreach my $d (@disks) { - print &ui_columns_row([ - "".&partition_description($d->{'device'})."", - &nice_size($d->{'size'}), - $d->{'model'}, - scalar(@{$d->{'slices'}}), - ]); - } - print &ui_columns_end(); - } -else { - print "$text{'index_none'} \n"; - } + print ui_columns_start([ + $text{'index_dname'}, + $text{'index_dsize'}, + $text{'index_dmodel'}, + $text{'index_dparts'} + ]); -&ui_print_footer("/", $text{'index'}); + foreach my $d (@disks) { + my $device = $d->{'device'} // ''; + my $disk_name = $device; $disk_name =~ s{^/dev/}{}; + # Prefer mediasize from gpart list (bytes); fallback to diskinfo size + my $base = $device; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + my $bytes = $ds && $ds->{'mediasize'} ? $ds->{'mediasize'} : $d->{'size'}; + my $size_display = defined $bytes ? safe_nice_size($bytes) : '-'; + my $model = $d->{'model'} // '-'; + my $url_device = urlize($device); + my $slices_cnt = scalar(@{ $d->{'slices'} || [] }); + + print ui_columns_row([ + "$disk_name", + $size_display, + $model, # Now correctly populated from bsdfdisk-lib.pl + $slices_cnt, + ]); + } + print ui_columns_end(); +} +else { + print "$text{'index_none'}\n"; +} + +ui_print_footer("/", $text{'index_return'}); \ No newline at end of file diff --git a/bsdfdisk/lang/en b/bsdfdisk/lang/en index 3583db17c..01d7be0d2 100644 --- a/bsdfdisk/lang/en +++ b/bsdfdisk/lang/en @@ -1,42 +1,92 @@ index_title=Partitions on Local Disks -index_ecmd=The required command $1 is missing -index_problem=This module cannot be used : $1 -index_none=No disks were found on this system! +index_ecmd=Missing required command $1 +index_problem=Cannot use this module : $1 +index_none=No disks found on this system! index_dname=Disk name index_dsize=Total size index_dmodel=Make and model index_dparts=Slices index_return=list of disks +index_format=Format +index_efdisk=You must have $1 or $2 installed to use this module +disk_edevice=Invalid device parameter +nslice_etype=Invalid slice type +# Disk overview disk_title=Edit Disk disk_egone=Disk no longer exists! -disk_no=Slice disk_type=Type disk_extent=Extent disk_start=Start block disk_end=End block -disk_use=Used by -disk_scount=$1 partitions disk_parts=Partitions disk_free=Free space -disk_vm=Virtual memory -disk_iscsi=iSCSI shared device $1 disk_none=This disk has no slices yet. -disk_size=Size -disk_dsize=Disk size: $1 -disk_model=Make and model: $1 -disk_cylinders=Cylinders: $1 -disk_blocks=Blocks: $1 -disk_device=Device file: $1 +disk_dsize=Disk size +disk_model=Make and model +disk_cylinders=Cylinders +disk_blocks=Blocks +disk_device=Device file disk_return=disk details and list of slices disk_add=Create a new slice. disk_smart=Show SMART Status disk_smartdesc=Show the current status of this drive as detected by SMART, and check it for disk errors. +disk_no=No. +disk_partno=Part. No. +disk_partname=Part. Name +disk_partlabel=Part. Label +disk_subpart=Sub-part. +disk_size=Size +disk_free_space=Free Space +disk_available=Available +disk_format=Format +disk_use=Used by +disk_role=Role Type -select_device=$1 device $2 -select_slice=$1 device $2 slice $3 -select_part=$1 device $2 slice $3 partition $4 +# Debug and scheme +disk_show_debug=Show raw debug information +disk_hide_debug=Hide debug information +disk_scheme=Partition Scheme +disk_unknown=Unknown +disk_sectorsize=Sector Size +disk_bytes=bytes +# GEOM information +disk_geom_header=GEOM Information +disk_geom_details=GEOM Disk Details +disk_mediasize=Media Size +disk_stripesize=Stripe Size +disk_stripeoffset=Stripe Offset +disk_mode=Mode +disk_rotationrate=Rotation Rate +disk_ident=Identifier +disk_lunid=LUN ID +disk_descr=Description +disk_rpm=RPM +disk_ssd=SSD (Solid State Drive) + +# Debug panels +disk_debug_gpart=GPART Output +disk_debug_structure=Parsed Disk Structure +disk_debug_geom=GEOM Disk List +disk_debug_zfs=ZFS Status +disk_debug_zfs_cache=ZFS Devices Cache + +# Roles and usage +disk_inzfs=In ZFS pool +disk_zfs_log=ZFS Log Device +disk_zfs_cache=ZFS Cache Device +disk_zfs_spare=ZFS Spare Device +disk_zfs_mirror=ZFS Mirror +disk_zfs_stripe=ZFS Stripe +disk_zfs_single=ZFS Single Device +disk_zfs_data=ZFS Data Device +disk_boot=Boot Partition +disk_boot_role=System Boot +disk_swap=Swap +disk_swap_role=Virtual Memory + +# Slice pages slice_title=Edit Slice slice_egone=Selected slice does not exist! slice_ssize=Slice size @@ -56,55 +106,70 @@ slice_use=Used by slice_none=This slice has no partitions yet. slice_none2=This slice has no partitions, and none can be created as it is in use as a filesystem. slice_delete=Delete Slice -slice_deletedesc=Delete this slice and all partitions and filesystems within it. Any data on those filesystem will be almost certainly unrecoverable. +slice_deletedesc=Delete this slice and all partitions and filesystems within it. Any data on that filesystem will be almost certainly unrecoverable. slice_return=slice details and list of partitions slice_err=Failed to modify slice slice_header=Slice details slice_suse=Directly used by +slice_adddesc=Create a new partition within this slice. +slice_cannotedit=This slice cannot be modified as it is currently in use. +slice_eboot=Cannot delete this slice as it contains boot partitions +slice_bootdesc=This slice contains bootloader code or kernel files needed to start the system +slice_role=Role +# Delete slice dialogs dslice_title=Delete Slice -dslice_rusure=Are you sure you want to delete the slice $1? Any partitions and filesystems within it will also be deleted. +dslice_rusure=Are you sure you want to delete the slice $1 ? Any partitions and filesystems within it will also be deleted. dslice_warn=Warning - this slice is currently used by : $1 dslice_confirm=Delete Now dslice_deleting=Deleting slice $1 .. dslice_failed=.. deletion failed : $1 dslice_done=.. done +# Create slice nslice_title=Create Slice nslice_header=New slice details nslice_number=Slice number +nslice_autonext=Will auto-select next index nslice_diskblocks=Disk size in blocks nslice_start=Starting block nslice_end=Ending block -nslice_type=New slice type -nslice_makepart=Create default partition? nslice_err=Failed to create slice -nslice_enumber=Missing or non-numeric slice number +nslice_enumber=Slice number must be a number nslice_eclash=A slice with number $1 already exists nslice_estart=Starting block must be a number nslice_eend=Ending block must be a number -nslice_erange=Starting block must be lower than the ending block -nslice_emax=Ending block cannot be larger than the disk size of $1 blocks +nslice_erange=Starting block must be before ending block +nslice_emax=Ending block cannot be larger than disk size of $1 blocks nslice_creating=Creating slice $1 on $2 .. -nslice_failed=.. slice creation failed : $1 +nslice_failed=.. creation failed : $1 nslice_done=.. slice added -nslice_parting=Create default partitions in slice $1 on $2 .. nslice_pfailed=.. partition creation failed : $1 nslice_pdone=.. partition added +nslice_existing_header=Existing slices on this disk +nslice_existing_parts=Existing partitions on this disk +nslice_enospace=No space on device left to create a slice! +epart_existing=Existing partitions on this slice +# Create partition npart_title=Create Partition npart_header=New partition details npart_letter=Partition letter npart_diskblocks=Slice size in blocks +npart_slicerel=(slice-relative) +npart_creserved=partition 'c' is reserved npart_type=Partition type npart_err=Failed to create partition -npart_eletter=Partition number must be a letter from A to D +npart_eletter=Partition letter must be a-h (excluding c) +npart_ereserved=Partition 'c' is reserved for the whole slice in BSD disklabels npart_eclash=A partition with letter $1 already exists -npart_emax=Ending block cannot be larger than the slice size of $1 blocks +npart_emax=Ending block cannot be larger than slice size of $1 blocks +npart_erange=Start block must be less than end block npart_creating=Creating partition $1 on $2 .. npart_failed=.. partition creation failed : $1 npart_done=.. partition added +# Edit partition part_title=Edit Partition part_egone=Partition no longer exists! part_header=Partition details @@ -128,18 +193,29 @@ part_err=Failed to save partition part_esave=Currently in use by $1 part_newmount=Mount Partition On: part_newmount2=Mount Partition -part_mountmsg=Mount this device on new directory on your system, so that it can be used to store files. A filesystem must have been already created on the device. -part_mountmsg2=Mount this device as virtual memory on your system, to increase the amount of memory available. +part_mountmsg=Mount this device on a new directory on your system, so that it can be used to store files. A filesystem must already be created on the device. +part_mountmsg2=Mount this device as virtual memory on your system to increase the amount of available memory. part_cannotedit=This partition cannot be modified as it is currently in use. +part_boot=Boot Partition +part_eboot=Cannot delete boot partitions as this may render the system unbootable +part_bootdesc=This partition contains bootloader code or kernel files needed to start the system +part_zfslog=ZFS Log Device +part_zfsdata=ZFS Data Device +part_mounted=Mounted on $1 +part_unused=Not in use +part_bootcannotedit=This partition cannot be modified as it is a boot partition. Changing it could render the system unbootable. +part_role=Role +# Delete partition dialogs dpart_title=Delete Partition -dpart_rusure=Are you sure you want to delete the partition $1? Any filesystems within it will also be deleted. +dpart_rusure=Are you sure you want to delete the partition $1 ? Any filesystems in it will also be deleted. dpart_warn=Warning - this partition is currently used by $1 dpart_confirm=Delete Now dpart_deleting=Deleting partition $1 .. dpart_failed=.. deletion failed : $1 dpart_done=.. done +# New filesystem newfs_title=Create Filesystem newfs_header=New filesystem details newfs_free=Space to reserve for root @@ -147,27 +223,36 @@ newfs_deffree=Default (8%) newfs_trim=Enable TRIM mode for SSDs newfs_label=Filesystem label newfs_none=None -newfs_create=Create Now newfs_err=Failed to create filesystem -newfs_efree=Space to reserve for root must be a percentage -newfs_elabel=Missing or invalid label +newfs_efree=Missing or invalid percentage of free space +newfs_elabel=Missing or invalid filesystem label newfs_creating=Creating filesystem on $1 .. -newfs_failed=.. creation failed! -newfs_done=.. created successfully +newfs_failed=.. creation failed : $1 +newfs_done=.. filesystem created +# FS check fsck_title=Check Filesystem +fsck_header=Filesystem check options +fsck_repair=Repair mode +fsck_fix=Only fix safe errors +fsck_fix2=Try to fix all errors fsck_err=Failed to check filesystem -fsck_checking=Checking filesystem on $1 .. +fsck_exec=Executing command $1 .. fsck_failed=.. check failed! -fsck_done=.. check completed with no errors found +fsck_done=.. check complete +fsck_checking=Checking filesystem on $1 .. -log_create_slice=Created slice $1 -log_delete_slice=Deleted slice $1 -log_modify_slice=Modified slice $1 -log_create_part=Created partition $1 -log_delete_part=Deleted partition $1 -log_modify_part=Modified partition $1 -log_newfs_part=Created filesystem on partition $1 -log_fsck_part=Checked filesystem on partition $1 +# Logging +action_create_slice=Created slice $1 +action_delete_slice=Deleted slice $1 +action_modify_slice=Modified slice $1 +action_create_part=Created partition $1 +action_delete_part=Deleted partition $1 +action_modify_part=Modified partition $1 +action_create_fs=Created filesystem on $1 +action_check_fs=Checked filesystem on $1 -__norefs=1 +# Generic +save=Save +yes=Yes +no=No diff --git a/bsdfdisk/newfs.cgi b/bsdfdisk/newfs.cgi index 384030035..e97cee1e5 100755 --- a/bsdfdisk/newfs.cgi +++ b/bsdfdisk/newfs.cgi @@ -27,13 +27,25 @@ else { $object = $slice; } +# Safety checks: do not run newfs on boot partitions or in-use devices +if (is_boot_partition($object)) { + &error($in{'part'} ne '' ? $text{'part_eboot'} : $text{'slice_eboot'}); +} +my @st_obj = &fdisk::device_status($object->{'device'}); +my $use_obj = &fdisk::device_status_link(@st_obj); +if (@st_obj && $st_obj[2]) { + &error(&text('part_esave', $use_obj)); +} + # Validate inputs my $newfs = { }; -$in{'free_def'} || $in{'free'} =~ /^\d+$/ && $in{'free'} <= 100 || +$in{'free_def'} || + ($in{'free'} =~ /^\d+$/ && $in{'free'} >= 0 && $in{'free'} <= 100) || &error($text{'newfs_efree'}); $newfs->{'free'} = $in{'free_def'} ? undef : $in{'free'}; $newfs->{'trim'} = $in{'trim'}; -$in{'label_def'} || $in{'label'} =~ /^\S+$/ || +$in{'label_def'} || + length($in{'label'}) > 0 || &error($text{'newfs_elabel'}); $newfs->{'label'} = $in{'label_def'} ? undef : $in{'label'}; @@ -44,12 +56,14 @@ print &text('newfs_creating', "$object->{'device'}"),"\n"; print "\n"; my $cmd = &get_create_filesystem_command($disk, $slice, $part, $newfs); &additional_log('exec', undef, $cmd); -my $fh = "CMD"; +my $fh; &open_execute_command($fh, $cmd, 2); -while(<$fh>) { - print &html_escape($_); - } -close($fh); +if ($fh) { + while(<$fh>) { + print &html_escape($_); + } + close($fh); +} print ""; if ($?) { print $text{'newfs_failed'},"\n"; @@ -58,6 +72,18 @@ else { print $text{'newfs_done'},"\n"; &webmin_log("newfs", $in{'part'} ne '' ? "part" : "object", $object->{'device'}, $object); + # If a label was provided, set the partition label (GPT slice or BSD sub-partition) + if (!$in{'label_def'} && defined $in{'label'} && length $in{'label'}) { + my $errlbl = set_partition_label( + disk => $disk, + slice => $slice, + part => ($in{'part'} ne '' ? $part : undef), + label => $in{'label'} + ); + if ($errlbl) { + print "Warning: failed to set partition label: \n" . &html_escape($errlbl) . "\n"; + } + } } if ($in{'part'} ne '') { @@ -69,4 +95,4 @@ else { &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". "slice=$in{'slice'}", $text{'slice_return'}); - } + } \ No newline at end of file diff --git a/bsdfdisk/newfs_form.cgi b/bsdfdisk/newfs_form.cgi index 7466a2448..5bff91f0d 100755 --- a/bsdfdisk/newfs_form.cgi +++ b/bsdfdisk/newfs_form.cgi @@ -10,21 +10,27 @@ our (%in, %text, $module_name); &ReadParse(); # Get the disk and slice -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; -$slice || &error($text{'slice_egone'}); -my $object; -if ($in{'part'} ne '') { - my ($part) = grep { $_->{'letter'} eq $in{'part'} } - @{$slice->{'parts'}}; - $part || &error($text{'part_egone'}); - $object = $part; - } -else { - $object = $slice; - } + # Validate input parameters to prevent command injection + $in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ or &error("Invalid device name"); + $in{'device'} !~ /\.\./ or &error("Invalid device name"); + $in{'slice'} =~ /^\d+$/ or &error("Invalid slice number") if $in{'slice'}; + $in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter") if $in{'part'}; + my @disks = &list_disks_partitions(); + my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; + $disk || &error($text{'disk_egone'}); + my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; + $slice || &error($text{'slice_egone'}); + my $object; + if ($in{'part'} ne '') { + $in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter"); + my ($part) = grep { $_->{'letter'} eq $in{'part'} } + @{$slice->{'parts'}}; + $part || &error($text{'part_egone'}); + $object = $part; + } + else { + $object = $slice; + } &ui_print_header($object->{'desc'}, $text{'newfs_title'}, ""); @@ -38,24 +44,24 @@ print &ui_table_row($text{'part_device'}, "$object->{'device'}"); print &ui_table_row($text{'newfs_free'}, - &ui_opt_textbox("free", undef, 4, $text{'newfs_deffree'})."%"); + &ui_opt_textbox("free", undef, 4, $text{'newfs_deffree'}) . "%"); print &ui_table_row($text{'newfs_trim'}, - &ui_yesno_radio("trim", 0)); + &ui_yesno_radio("trim", 0)); print &ui_table_row($text{'newfs_label'}, - &ui_opt_textbox("label", undef, 20, $text{'newfs_none'})); + &ui_opt_textbox("label", undef, 20, $text{'newfs_none'})); print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'newfs_create'} ] ]); +print &ui_form_end([ [ undef, $text{'save'} ] ]); if ($in{'part'} ne '') { - &ui_print_footer("edit_part.cgi?device=$in{'device'}&". - "slice=$in{'slice'}&part=$in{'part'}", - $text{'part_return'}); - } + &ui_print_footer("edit_part.cgi?device=$in{'device'}&" . + "slice=$in{'slice'}&part=$in{'part'}", + $text{'part_return'}); + } else { - &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". - "slice=$in{'slice'}", - $text{'slice_return'}); - } + &ui_print_footer("edit_slice.cgi?device=$in{'device'}&" . + "slice=$in{'slice'}", + $text{'slice_return'}); + } \ No newline at end of file diff --git a/bsdfdisk/part_form.cgi b/bsdfdisk/part_form.cgi index f6eb6fefe..57a8661c3 100755 --- a/bsdfdisk/part_form.cgi +++ b/bsdfdisk/part_form.cgi @@ -23,37 +23,65 @@ print &ui_hidden("device", $in{'device'}); print &ui_hidden("slice", $in{'slice'}); print &ui_table_start($text{'npart_header'}, undef, 2); -# Partition number (first free) +# Partition number (first free, skipping 'c' which is reserved for the whole slice) my %used = map { $_->{'letter'}, $_ } @{$slice->{'parts'}}; +$used{'c'} = 1; # Reserve 'c' for the whole slice (BSD convention) my $l = 'a'; while($used{$l}) { $l++; } print &ui_table_row($text{'npart_letter'}, - &ui_textbox("letter", $l, 4)); + &ui_textbox("letter", $l, 4) . " (" . $text{'npart_creserved'} . ")"); # Slice size in blocks print &ui_table_row($text{'npart_diskblocks'}, $slice->{'blocks'}); -# Start and end blocks (defaults to last part) -my ($start, $end) = (0, $slice->{'blocks'}); -foreach my $p (sort { $a->{'startblock'} cmp $b->{'startblock'} } - @{$slice->{'parts'}}) { - $start = $p->{'startblock'} + $p->{'blocks'} + 1; - } -print &ui_table_row($text{'nslice_start'}, - &ui_textbox("start", $start, 10)); -print &ui_table_row($text{'nslice_end'}, - &ui_textbox("end", $end, 10)); - +# Start and end blocks for BSD partitions are SLICE-RELATIVE (not disk-absolute) +# Start at 0 (or after last partition), end at slice size - 1 +my ($start, $end) = (0, $slice->{'blocks'} - 1); +foreach my $p (sort { $a->{'startblock'} <=> $b->{'startblock'} } + @{$slice->{'parts'}}) { + # Partitions are already stored as slice-relative + $start = $p->{'startblock'} + $p->{'blocks'}; +} +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'} . " " . $text{'npart_slicerel'}, + &ui_textbox("start", $start, 10)); +print &ui_table_row($text{'nslice_end'} . " " . $text{'npart_slicerel'}, + &ui_textbox("end", $end, 10)); + # Partition type +# For BSD-on-MBR inner label partitions, offer FreeBSD partition types +my $scheme = 'BSD'; +my $default_ptype = 'freebsd-ufs'; print &ui_table_row($text{'npart_type'}, - &ui_select("type", '4.2BSD', - [ &list_partition_types() ])); - + &ui_select("type", $default_ptype, + [ list_partition_types($scheme) ])); + print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'create'} ] ]); +print &ui_form_end([ [ undef, $text{'save'} ] ]); +# Existing partitions summary +if (@{$slice->{'parts'}||[]}) { + my $zfs = get_all_zfs_info(); + print &ui_hr(); + print &ui_columns_start([ + $text{'slice_letter'}, $text{'slice_type'}, $text{'slice_start'}, $text{'slice_end'}, $text{'slice_size'}, $text{'slice_use'}, $text{'slice_role'} + ], $text{'epart_existing'}); + foreach my $p (sort { $a->{'startblock'} <=> $b->{'startblock'} } @{$slice->{'parts'}}) { + my $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + my @stp = fdisk::device_status($p->{'device'}); + my $usep = $zfs->{$p->{'device'}} || fdisk::device_status_link(@stp) || $text{'part_nouse'}; + my $rolep = get_partition_role($p); + my $pb = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + print &ui_columns_row([ + uc($p->{'letter'}), $ptype, $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, ($pb ? safe_nice_size($pb) : '-'), $usep, $rolep + ]); + } + print &ui_columns_end(); +} + &ui_print_footer("edit_slice.cgi?device=$in{'device'}&slice=$in{'slice'}", - $text{'slice_return'}); + $text{'slice_return'}); \ No newline at end of file diff --git a/bsdfdisk/save_slice.cgi b/bsdfdisk/save_slice.cgi index 69cbda19f..22c2540ef 100755 --- a/bsdfdisk/save_slice.cgi +++ b/bsdfdisk/save_slice.cgi @@ -20,9 +20,22 @@ $slice || &error($text{'slice_egone'}); # Apply changes my $oldslice = { %$slice }; $slice->{'type'} = $in{'type'}; -if (!$slice->{'active'}) { - $slice->{'active'} = $in{'active'}; - } +$slice->{'active'} = $in{'active'} if (defined $in{'active'}); + +# Apply active flag for MBR disks via gpart set/unset when it changed +my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; +my $ds = get_disk_structure($base); +if (is_using_gpart() && $ds && $ds->{'scheme'} && $ds->{'scheme'} !~ /GPT/i) { + my $idx = slice_number($slice); + if (defined $oldslice->{'active'} && defined $slice->{'active'} && $oldslice->{'active'} != $slice->{'active'}) { + my $cmd = $slice->{'active'} ? "gpart set -a active -i $idx $base" : "gpart unset -a active -i $idx $base"; + my $out = `$cmd 2>&1`; + if ($? != 0) { + &error("Failed to change active flag: $out"); + } + } +} + my $err = &modify_slice($disk, $oldslice, $slice); &error($err) if ($err); diff --git a/bsdfdisk/slice_form.cgi b/bsdfdisk/slice_form.cgi index 904310e43..14a6d917c 100755 --- a/bsdfdisk/slice_form.cgi +++ b/bsdfdisk/slice_form.cgi @@ -16,6 +16,29 @@ $disk || &error($text{'disk_egone'}); &ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); +# Determine scheme for read-only behavior and note +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = get_disk_structure($base_device); +my $is_gpt = (is_using_gpart() && $disk_structure && ($disk_structure->{'scheme'}||'') =~ /GPT/i); + +# Check if there is any free space on the device +my $has_free_space = 0; +if ($disk_structure && $disk_structure->{'entries'}) { + foreach my $entry (@{$disk_structure->{'entries'}}) { + if ($entry->{'type'} eq 'free' && $entry->{'size'} > 0) { + $has_free_space = 1; + last; + } + } +} + +# If no free space, show error and return +if (!$has_free_space) { + print "$text{'nslice_enospace'}\n"; + &ui_print_footer("edit_disk.cgi?device=$in{'device'}", $text{'disk_return'}); + exit; +} + print &ui_form_start("create_slice.cgi", "post"); print &ui_hidden("device", $in{'device'}); print &ui_table_start($text{'nslice_header'}, undef, 2); @@ -26,37 +49,86 @@ my $n = 1; while($used{$n}) { $n++; } -print &ui_table_row($text{'nslice_number'}, - &ui_textbox("number", $n, 6)); - -# Disk size in blocks -print &ui_table_row($text{'nslice_diskblocks'}, - $disk->{'blocks'}); - -# Start and end blocks (defaults to last slice+1) -my ($start, $end) = (63, $disk->{'blocks'}); -foreach my $s (sort { $a->{'startblock'} cmp $b->{'startblock'} } +my $num_field = $is_gpt + ? " ".$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'});
\n"; -my $err = &save_partition($disk, $slice, $part); +# Actually create the partition inside the slice (initialize BSD label if needed) +my $err = &create_partition($disk, $slice, $part); if ($err) { print &text('npart_failed', $err),"
\n"; } diff --git a/bsdfdisk/create_slice.cgi b/bsdfdisk/create_slice.cgi index b9e7a1961..e8480f3a3 100755 --- a/bsdfdisk/create_slice.cgi +++ b/bsdfdisk/create_slice.cgi @@ -1,65 +1,109 @@ #!/usr/local/bin/perl -# Actually create a new slice - use strict; use warnings; no warnings 'redefine'; -no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); -&ReadParse(); -&error_setup($text{'nslice_err'}); - -# Get the disk -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); - +ReadParse(); +error_setup($text{'nslice_err'}); +# Get the disk using first() for an early exit on match +my @disks = list_disks_partitions(); +my $disk; +foreach my $d (@disks) { + if ($d->{'device'} eq $in{'device'}) { + $disk = $d; + last; + } +} +# Validate device parameter to prevent path traversal and command injection +$disk or error($text{'disk_egone'}); +# Prefer GPART total blocks for bounds +(my $base_dev = $in{'device'}) =~ s{^/dev/}{}; +my $ds = get_disk_structure($base_dev); +my $disk_blocks = ($ds && $ds->{'total_blocks'}) ? $ds->{'total_blocks'} : ($disk->{'blocks'} || 0); # Validate inputs, starting with slice number -my $slice = { }; -$in{'number'} =~ /^\d+$/ || &error($text{'nslice_enumber'}); -my ($clash) = grep { $_->{'number'} == $in{'number'} } @{$disk->{'slices'}}; -$clash && &error(&text('nslice_eclash', $in{'number'})); +my $slice = {}; +$in{'number'} =~ /^\d+$/ or error($text{'nslice_enumber'}); +# Check for clash using first() with a loop exiting on first match +my $clash; +foreach my $s (@{$disk->{'slices'}}) { + if ($s->{'number'} == $in{'number'}) { + $clash = $s; + last; + } +} $slice->{'number'} = $in{'number'}; - # Start and end blocks -$in{'start'} =~ /^\d+$/ || &error($text{'nslice_estart'}); -$in{'end'} =~ /^\d+$/ || &error($text{'nslice_eend'}); -$in{'start'} < $in{'end'} || &error($text{'nslice_erange'}); +$in{'start'} =~ /^\d+$/ or error($text{'nslice_estart'}); +$in{'end'} =~ /^\d+$/ or error($text{'nslice_eend'}); +($in{'start'} < $in{'end'}) or error($text{'nslice_erange'}); +# total_blocks is the block *after* the last valid block, so end must be < total_blocks +($in{'end'} < $disk_blocks) or error(text('nslice_emax', $disk_blocks - 1)); + +# Ensure the new slice does not overlap existing slices +foreach my $s (@{ $disk->{'slices'} }) { + my $s_start = $s->{'startblock'}; + my $s_end = $s->{'startblock'} + $s->{'blocks'} - 1; + if (!($in{'end'} < $s_start || $in{'start'} > $s_end)) { + error("Requested slice range overlaps with existing slice #".$s->{'number'}); + } +} + $slice->{'startblock'} = $in{'start'}; -$slice->{'blocks'} = $in{'end'} - $in{'start'}; - -# Slice type +$slice->{'blocks'} = $in{'end'} - $in{'start'} + 1; + # Slice type +$in{'type'} =~ /^[a-zA-Z0-9_-]+$/ or error($text{'nslice_etype'}); +length($in{'type'}) <= 20 or error($text{'nslice_etype'}); $slice->{'type'} = $in{'type'}; - # Do the creation -&ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); - -print &text('nslice_creating', $in{'number'}, $disk->{'desc'}),"
\n"; -my $err = &create_slice($disk, $slice); +ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); +print text('nslice_creating', $in{'number'}, $disk->{'desc'}), "
\n"; +my $err = create_slice($disk, $slice); if ($err) { - print &text('nslice_failed', $err),"
\n"; - } -else { - print &text('nslice_done'),"
\n"; - } - + print text('nslice_failed', $err), "
\n"; +} else { + print text('nslice_done'), "
\n"; + # Auto-label the new partition provider with its name if scheme is GPT or BSD + my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + if ($ds && $ds->{'scheme'}) { + # Determine provider and label text + my $label_text = slice_name($slice); # e.g., da8s2 or da0p2 + if ($ds->{'scheme'} =~ /GPT/i) { + my $idx = $slice->{'number'}; + if ($idx) { + my $cmd2 = "gpart modify -i $idx -l " . quote_path($label_text) . " $base"; + my $out2 = `$cmd2 2>&1`; + # If it fails, ignore silently + } + } else { + # On MBR, if BSD label exists we can set label once created; ignore for now + } + } +} if (!$err && $in{'makepart'}) { - # Also create a partition - print &text('nslice_parting', $in{'number'}, $disk->{'desc'}),"
\n"; - my $err = &initialize_slice($disk, $slice); - if ($err) { - print &text('nslice_pfailed', $err),"
\n"; - } - else { - print &text('nslice_pdone'),"
\n"; - } - } - + # Also create a partition (initialize slice label) + my $part_err = initialize_slice($disk, $slice); + if ($part_err) { + print text('nslice_pfailed', $part_err), "
\n"; + } else { + print text('nslice_pdone'), "
\n"; + } +} if (!$err) { - &webmin_log("create", "slice", $slice->{'device'}, $slice); - } - -&ui_print_footer("edit_disk.cgi?device=$in{'device'}", - $text{'disk_return'}); - + # Auto-label GPT partitions with their device name (e.g., da8p2) + my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + if ($ds && $ds->{'scheme'} && $ds->{'scheme'} =~ /GPT/i) { + my $slice_devname = $slice->{'device'}; + $slice_devname =~ s{^/dev/}{}; # e.g., da8p2 + my $idx = $slice->{'number'}; + if ($idx && $slice_devname) { + my $label_cmd = "gpart modify -i $idx -l " . quote_path($slice_devname) . " $base 2>&1"; + my $label_out = `$label_cmd`; + # Ignore errors - labeling is optional + } + } + webmin_log("create", "slice", $slice->{'device'}, $slice); +} +ui_print_footer("edit_disk.cgi?device=$in{'device'}", $text{'disk_return'}); \ No newline at end of file diff --git a/bsdfdisk/edit_disk.cgi b/bsdfdisk/edit_disk.cgi index 66a94458b..8a7b0152c 100755 --- a/bsdfdisk/edit_disk.cgi +++ b/bsdfdisk/edit_disk.cgi @@ -1,6 +1,5 @@ #!/usr/local/bin/perl # Show details of a disk, and slices on it - use strict; use warnings; no warnings 'redefine'; @@ -8,93 +7,334 @@ no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); &ReadParse(); -my $extwidth = 300; - +my $extwidth = 100; # Get the disk my @disks = &list_disks_partitions(); my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; $disk || &error($text{'disk_egone'}); - +# Cache commonly used values +my $device = $disk->{'device'}; +my $device_url = &urlize($device); +my $desc = $disk->{'desc'}; +# Prefer total blocks from gpart header when available +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = &get_disk_structure($base_device); +my $disk_blocks = ($disk_structure && $disk_structure->{'total_blocks'}) ? $disk_structure->{'total_blocks'} : ($disk->{'blocks'} || 1000000); +# Precompute a scale factor for extent image widths +my $scale = $extwidth / ($disk_blocks || 1); &ui_print_header($disk->{'desc'}, $text{'disk_title'}, ""); - -# Show disk details -my @info = ( ); -push(@info, &text('disk_dsize', &nice_size($disk->{'size'}))); +# Debug toggle bar +print "
\n"; + } + if ($geom_info->{'ident'}) { + print &ui_table_row($text{'disk_ident'}, $geom_info->{'ident'}); + } + if ($geom_info->{'lunid'}) { + print &ui_table_row($text{'disk_lunid'}, $geom_info->{'lunid'}); + } + if ($geom_info->{'descr'}) { + print &ui_table_row($text{'disk_descr'}, $geom_info->{'descr'}); + } +} +# Advanced information (cylinders, blocks) +print &ui_table_hr(); +print &ui_table_row($text{'disk_advanced_header'}, "$text{'disk_advanced_details'}", 2); +if ($disk->{'cylinders'}) { + print &ui_table_row($text{'disk_cylinders'}, $disk->{'cylinders'}); +} +print &ui_table_row($text{'disk_blocks'}, $disk->{'blocks'}); +print &ui_table_end(); +# Debug: print raw outputs if debug mode is enabled +if ($in{'debug'}) { + print "
Command: $cmd\nOutput:\n$out\n
Disk Structure:\n"; + foreach my $key (sort keys %$disk_structure) { + if ($key eq 'entries') { + print "entries: [\n"; + foreach my $entry (@{$disk_structure->{'entries'}}) { + print " {\n"; + foreach my $k (sort keys %$entry) { + print " $k: $entry->{$k}\n"; + } + print " },\n"; + } + print "]\n"; + } else { + print "$key: $disk_structure->{$key}\n"; + } + } + print "
Raw GEOM output:\n"; + print &html_escape(&backquote_command("geom disk list " . "e_path($device) . " 2>/dev/null")); + print "
"; + my $cmd = "zpool status 2>&1"; + my $out = &backquote_command($cmd); + print "Command: $cmd\nOutput:\n$out\n"; + print "
Partition Details Mapping:\n"; + foreach my $pnum (sort { $a <=> $b } keys %part_details) { + print " $pnum: {\n"; + foreach my $k (sort keys %{$part_details{$pnum}}) { + print " $k: $part_details{$pnum}->{$k}\n"; + } + print " },\n"; + } + print "
\n"; - } +my @links = ( "".$text{'disk_add'}."" ); +if (@$entries) { + print &ui_links_row(\@links); + print &ui_columns_start([ + $text{'disk_no'}, # Row number + $text{'disk_partno'}, # Part. No. + $text{'disk_partname'}, # Part. Name + $text{'disk_partlabel'}, # Part. Label + $text{'disk_subpart'}, # Sub-part. + $text{'disk_extent'}, # Extent + $text{'disk_start'}, # Startblock + $text{'disk_end'}, # Endblock + $text{'disk_size'}, # Size + $text{'disk_format'}, # Format type + $text{'disk_use'}, # Used by + $text{'disk_role'}, # Role Type + ]); + my $row_number = 1; + foreach my $entry (@$entries) { + my @cols = (); + push(@cols, $row_number++); + if ($entry->{'type'} eq 'free') { + my $start = $entry->{'start'}; + my $end = $entry->{'start'} + $entry->{'size'} - 1; + my $create_url = "slice_form.cgi?device=$device_url&new=1&start=$start&end=$end"; + push(@cols, "".$text{'disk_free'}.""); + push(@cols, "-"); + push(@cols, "-"); + push(@cols, "-"); + my $ext = ""; + $ext .= sprintf "", $scale * ($entry->{'start'} - 1); + $ext .= sprintf "", $scale * ($entry->{'size'}); + $ext .= sprintf "", $scale * ($disk_blocks - $entry->{'start'} - $entry->{'size'}); + push(@cols, $ext); + push(@cols, $start); + push(@cols, $end); + push(@cols, $entry->{'size_human'}); + push(@cols, $text{'disk_free_space'}); + push(@cols, $text{'disk_available'}); + push(@cols, "-"); + } else { + my $part_num = $entry->{'index'}; + my $ext = ""; + $ext .= sprintf "", $scale * ($entry->{'start'} - 1); + $ext .= sprintf "", $scale * ($entry->{'size'}); + $ext .= sprintf "", $scale * ($disk_blocks - $entry->{'start'} - $entry->{'size'}); + my $url = "edit_slice.cgi?device=$device_url&slice=".&urlize($part_num); + push(@cols, "".&html_escape($part_num).""); + + my $part_info = $part_details{$part_num}; + my $part_name = $part_info ? $part_info->{'name'} : "-"; + push(@cols, $part_name); + my $part_label = $part_info ? $part_info->{'label'} : ($entry->{'label'} eq "(null)" ? "-" : $entry->{'label'}); + push(@cols, $part_label); + + # Find sub-partitions if available + my ($slice) = grep { $_->{'number'} eq $part_num } @{$disk->{'slices'} || []}; + my $sub_part_info = ($slice && scalar(@{$slice->{'parts'}||[]}) > 0) ? + join(", ", map { $_->{'letter'} } @{$slice->{'parts'}}) : "-"; + push(@cols, $sub_part_info); + + push(@cols, $ext); + push(@cols, $entry->{'start'}); + push(@cols, $entry->{'start'} + $entry->{'size'} - 1); + push(@cols, $entry->{'size_human'}); + + # Classify format/use/role via library helper + my ($format_type, $usage, $role) = classify_partition_row( + base_device => $base_device, + scheme => ($disk_structure->{'scheme'} || ''), + part_num => $part_num, + part_name => $part_name, + part_label => $part_label, + entry_part_type => ($part_info ? $part_info->{'type'} : $entry->{'part_type'}), + entry_rawtype => ($part_info ? $part_info->{'rawtype'} : undef), + size_human => $entry->{'size_human'}, + size_blocks => $entry->{'size'}, + zfs_devices => $zfs_devices, + ); + push(@cols, $format_type || '-'); + push(@cols, $usage || $text{'part_nouse'}); + push(@cols, $role || '-'); + } + print &ui_columns_row(\@cols); + } + print &ui_columns_end(); +} else { + if (@{$disk->{'slices'}||[]}) { + print &ui_links_row(\@links); + print &ui_columns_start([ + $text{'disk_no'}, + $text{'disk_type'}, + $text{'disk_extent'}, + $text{'disk_start'}, + $text{'disk_end'}, + $text{'disk_use'}, + ]); + foreach my $s (@{$disk->{'slices'}}) { + my @cols = (); + my $ext = ""; + $ext .= sprintf "", $scale * ($s->{'startblock'} - 1); + $ext .= sprintf "", ($s->{'extended'} ? "ext" : "use"), $scale * ($s->{'blocks'}); + $ext .= sprintf "", $scale * ($disk_blocks - $s->{'startblock'} - $s->{'blocks'}); + my $url = "edit_slice.cgi?device=$device_url&slice=".&urlize($s->{'number'}); + push(@cols, "".&html_escape($s->{'number'}).""); + push(@cols, &get_type_description($s->{'type'}) || $s->{'type'}); + push(@cols, $ext); + push(@cols, $s->{'startblock'}); + push(@cols, $s->{'startblock'} + $s->{'blocks'} - 1); + my @st = &fdisk::device_status($s->{'device'}); + my $use = &fdisk::device_status_link(@st); + push(@cols, $use || $text{'part_nouse'}); + print &ui_columns_row(\@cols); + } + print &ui_columns_end(); + } else { + print "
$text{'disk_none'}
Pools: " . join(", ", keys %$zfs_pools) . "\n\nDevices:\n"; + foreach my $device_id (sort keys %$zfs_devices) { + next if $device_id =~ /^_debug_/; + my $device_info = $zfs_devices->{$device_id}; + print "$device_id => Pool: $device_info->{'pool'}, Type: $device_info->{'vdev_type'}, Mirrored: " . + ($device_info->{'is_mirrored'} ? "Yes" : "No") . ", RAIDZ: " . + ($device_info->{'is_raidz'} ? "Yes (Level: $device_info->{'raidz_level'})" : "No") . + ", Single: " . ($device_info->{'is_single'} ? "Yes" : "No") . + ", Striped: " . ($device_info->{'is_striped'} ? "Yes" : "No") . "\n"; + } + print "
\n"; - } - -&ui_print_footer("edit_slice.cgi?device=$in{'device'}&slice=$in{'slice'}", - $text{'slice_return'}); + print ui_hr(); + print ui_buttons_start(); + show_filesystem_buttons($hiddens, \@st, $part); + print ui_buttons_row("delete_part.cgi", $text{'part_delete'}, $text{'part_deletedesc'}, $hiddens); + print ui_buttons_end(); +} else { + print ($is_boot) ? "$text{'part_bootcannotedit'}
\n" + : "$text{'part_cannotedit'}
\n"; +} +# SMART button (physical device) +if (&has_command("smartctl")) { + print ui_hr(); + print ui_buttons_start(); + print ui_buttons_row("smart.cgi", $text{'disk_smart'}, $text{'disk_smartdesc'}, + ui_hidden("device", $disk->{'device'})); + print ui_buttons_end(); +} +ui_print_footer("edit_slice.cgi?device=$device&slice=$slice_num", $text{'slice_return'}); \ No newline at end of file diff --git a/bsdfdisk/edit_slice.cgi b/bsdfdisk/edit_slice.cgi index 3a901154c..931695384 100755 --- a/bsdfdisk/edit_slice.cgi +++ b/bsdfdisk/edit_slice.cgi @@ -1,146 +1,178 @@ #!/usr/local/bin/perl -# Show details of a slice, and partitions on it - use strict; use warnings; no warnings 'redefine'; no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, $module_name); -&ReadParse(); +ReadParse(); my $extwidth = 300; - # Get the disk and slice -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; -$slice || &error($text{'slice_egone'}); - -&ui_print_header($slice->{'desc'}, $text{'slice_title'}, ""); - +my @disks = list_disks_partitions(); +my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks or error($text{'disk_egone'}); +my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}} or error($text{'slice_egone'}); +ui_print_header($slice->{'desc'}, $text{'slice_title'}, ""); # Show slice details -my @st = &fdisk::device_status($slice->{'device'}); -my $use = &fdisk::device_status_link(@st); -my $canedit = !@st || !$st[2]; -my $hiddens = &ui_hidden("device", $in{'device'})."\n". - &ui_hidden("slice", $in{'slice'})."\n"; -print &ui_form_start("save_slice.cgi"); +my $zfs_info = get_all_zfs_info(); +my ($zfs_pools, $zfs_devices) = build_zfs_devices_cache(); +# Cache slice device status +my @slice_status = fdisk::device_status($slice->{'device'}); +my $slice_use = $zfs_info->{$slice->{'device'}} ? $zfs_info->{$slice->{'device'}} : fdisk::device_status_link(@slice_status); +my $canedit = (! @slice_status || !$slice_status[2]); +# Prepare hidden fields +my $hiddens = ui_hidden("device", $in{'device'}) . "\n" . ui_hidden("slice", $in{'slice'}) . "\n"; +# Derive disk scheme for classifier +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = get_disk_structure($base_device); +# Check if this is a boot slice +my $is_boot = is_boot_partition($slice); +print ui_alert_box($text{'slice_bootdesc'}, 'info') if $is_boot; +print ui_form_start("save_slice.cgi"); print $hiddens; -print &ui_table_start($text{'slice_header'}, undef, 2); - -print &ui_table_row($text{'part_device'}, - "$slice->{'device'}"); - -print &ui_table_row($text{'slice_ssize'}, - &nice_size($slice->{'size'})); - -print &ui_table_row($text{'slice_sstart'}, - $slice->{'startblock'}); - -print &ui_table_row($text{'slice_send'}, - $slice->{'startblock'} + $slice->{'blocks'} - 1); - -print &ui_table_row($text{'slice_stype'}, - &ui_select("type", $slice->{'type'}, - [ sort { $a->[1] cmp $b->[1] } - map { [ $_, &fdisk::tag_name($_) ] } - &fdisk::list_tags() ])); - -print &ui_table_row($text{'slice_sactive'}, - $slice->{'active'} ? $text{'yes'} : - &ui_yesno_radio("active", $slice->{'active'})); - -print &ui_table_row($text{'slice_suse'}, - !@st ? $text{'part_nouse'} : - $st[2] ? &text('part_inuse', $use) : - &text('part_foruse', $use)); - -print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'save'} ] ]); - -print &ui_hr(); - -# Show partitions table -my @links = ( "".$text{'slice_add'}."" ); -if (@{$slice->{'parts'}}) { - print &ui_links_row(\@links); - print &ui_columns_start([ - $text{'slice_letter'}, - $text{'slice_type'}, - $text{'slice_extent'}, - $text{'slice_size'}, - $text{'slice_start'}, - $text{'slice_end'}, - $text{'slice_use'}, - ]); - foreach my $p (@{$slice->{'parts'}}) { - # Create images for the extent - my $ext = ""; - $ext .= sprintf "", - $extwidth*($p->{'startblock'} - 1) / - $slice->{'blocks'}; - $ext .= sprintf "", - $p->{'extended'} ? "ext" : "use", - $extwidth*($p->{'blocks'}) / - $slice->{'blocks'}; - $ext .= sprintf "", - $extwidth*($slice->{'blocks'} - $p->{'startblock'} - - $p->{'blocks'}) / $slice->{'blocks'}; - - # Work out use - my @st = &fdisk::device_status($p->{'device'}); - my $use = &fdisk::device_status_link(@st); - - # Add row for the partition - my $url = "edit_part.cgi?device=".&urlize($disk->{'device'}). - "&slice=".$slice->{'number'}."&part=".$p->{'letter'}; - print &ui_columns_row([ - "".uc($p->{'letter'})."", - "$p->{'type'}", - $ext, - &nice_size($p->{'size'}), - $p->{'startblock'}, - $p->{'startblock'} + $p->{'blocks'} - 1, - $use, - ]); - } - print &ui_columns_end(); - print &ui_links_row(\@links); - } +print ui_table_start($text{'slice_header'}, undef, 2); +print ui_table_row($text{'part_device'}, "$slice->{'device'}"); +my $slice_bytes = bytes_from_blocks($slice->{'device'}, $slice->{'blocks'}); +print ui_table_row($text{'slice_ssize'}, $slice_bytes ? safe_nice_size($slice_bytes) : '-'); +print ui_table_row($text{'slice_sstart'}, $slice->{'startblock'}); +print ui_table_row($text{'slice_send'}, $slice->{'startblock'} + $slice->{'blocks'} - 1); +# Slice type selector (GPT vs legacy) +if (is_using_gpart()) { + my $scheme = ($disk_structure && $disk_structure->{'scheme'}) ? $disk_structure->{'scheme'} : 'GPT'; + my @opts = list_partition_types($scheme); + # Default sensibly per scheme + my $default_type = ($scheme =~ /GPT/i) ? 'freebsd-zfs' : 'freebsd'; + print ui_table_row($text{'slice_stype'}, ui_select("type", $slice->{'type'} || $default_type, \@opts)); +} else { - # No partitions yet - if (@st) { - # And directly in use, so none can be created - print "$text{'slice_none2'}
\n"; - } - else { - # Show link to add first partition - print "$text{'slice_none'}
\n"; - print &ui_links_row(\@links); - } - } + # Pre-cache tag options for the slice type select (legacy fdisk) + my @tags = fdisk::list_tags(); + my @tag_options = map { [ $_, fdisk::tag_name($_) ] } @tags; + @tag_options = sort { $a->[1] cmp $b->[1] } @tag_options; + print ui_table_row($text{'slice_stype'}, ui_select("type", $slice->{'type'}, \@tag_options)); +} +# Active slice - only applicable for legacy MBR. For GPT/UEFI and for EFI/freebsd-boot types, the active flag is irrelevant. +my $is_gpt = is_using_gpart() && ($disk_structure && $disk_structure->{'scheme'} && $disk_structure->{'scheme'} =~ /GPT/i); +if (!$is_gpt && ($slice->{'type'} !~ /^(?:efi|freebsd-boot)$/i)) { + my $active_default = $slice->{'active'} ? 1 : 0; + print ui_table_row($text{'slice_sactive'}, ui_yesno_radio("active", $active_default)); +} else { + # Do not offer the control; display 'No' since active is not used here + print ui_table_row($text{'slice_sactive'}, $text{'no'}); +} +print ui_table_row($text{'slice_suse'}, + (!$slice_use || $slice_use eq $text{'part_nouse'}) + ? $text{'part_nouse'} + : ($slice_status[2] ? text('part_inuse', $slice_use) : text('part_foruse', $slice_use))); +# Add a row for the slice role +print ui_table_row($text{'slice_role'}, get_partition_role($slice)); +print ui_table_end(); +print ui_form_end([ [ undef, $text{'save'} ] ]); +print ui_hr(); +# Show partitions table (only for MBR slices that support BSD disklabel) +my $can_have_parts = 0; +if (!is_using_gpart()) { + # Legacy MBR with BSD disklabel + $can_have_parts = 1; +} elsif ($disk_structure && $disk_structure->{'scheme'} && $disk_structure->{'scheme'} !~ /GPT/i) { + # MBR-style slice + $can_have_parts = 1; +} +my @links = $can_have_parts ? ( "" . $text{'slice_add'} . "" ) : (); +if (@{$slice->{'parts'}}) { + print ui_links_row(\@links) if @links; + print ui_columns_start([ + $text{'slice_letter'}, + $text{'slice_type'}, + $text{'slice_extent'}, + $text{'slice_size'}, + $text{'slice_start'}, + $text{'slice_end'}, + $text{'disk_stripesize'}, + $text{'slice_use'}, + $text{'slice_role'}, + ]); + + # Pre-calculate scaling factor for the partition extent images + my $scale = $extwidth / $slice->{'blocks'}; + + foreach my $p (@{$slice->{'parts'}}) { + # Create images representing the partition extent + my $gap_before = sprintf("", int($scale * ($p->{'startblock'} - 1))); + my $img_type = $p->{'extended'} ? "ext" : "use"; + my $partition_img = sprintf("", $img_type, int($scale * $p->{'blocks'})); + my $gap_after = sprintf("", int($scale * ($slice->{'blocks'} - $p->{'startblock'} - $p->{'blocks'}))); + my $ext = $gap_before . $partition_img . $gap_after; + + # Cache partition device status information + my @part_status = fdisk::device_status($p->{'device'}); + my $part_use = $zfs_info->{$p->{'device'}} || fdisk::device_status_link(@part_status); + # Prefer GEOM details for stripesize + my $ginfo = get_detailed_disk_info($p->{'device'}); + my $stripesize = ($ginfo && $ginfo->{'stripesize'}) ? $ginfo->{'stripesize'} : '-'; + + # Classify format/use/role via library helper + (my $pn = $p->{'device'}) =~ s{^/dev/}{}; + my ($fmt, $use_txt, $role_txt) = classify_partition_row( + base_device => $base_device, + scheme => ($disk_structure->{'scheme'} || ''), + part_name => $pn, + entry_part_type => $p->{'type'}, + zfs_devices => $zfs_devices, + ); + $use_txt ||= $part_use; + $role_txt ||= get_partition_role($p); + + # Build edit URL + my $url = "edit_part.cgi?device=" . urlize($disk->{'device'}) . "&slice=" . $slice->{'number'} . "&part=" . $p->{'letter'}; + my $psz_b = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + print ui_columns_row([ + "" . uc($p->{'letter'}) . "", + "" . ($fmt || get_format_type($p)) . "", + $ext, + ($psz_b ? safe_nice_size($psz_b) : '-'), + $p->{'startblock'}, + $p->{'startblock'} + $p->{'blocks'} - 1, + $stripesize, + $use_txt, + $role_txt, + ]); + } + print ui_columns_end(); + print ui_links_row(\@links) if @links; +} else { + # GPT partitions do not have sub-partitions + if (!$can_have_parts) { + # No message needed for GPT; partitions are top-level + } + # If slice is in use by a filesystem OR it is a boot slice, do not allow creating partitions + elsif (@slice_status || $zfs_info->{$slice->{'device'}} || $is_boot) { + print "$text{'slice_none2'}
\n"; + } else { + print "$text{'slice_none'}
\n"; + print ui_links_row(\@links) if @links; + } +} +if ($canedit && !$is_boot) { # Do not allow editing boot slices + print ui_hr(); + print ui_buttons_start(); + if (!@{$slice->{'parts'}}) { + show_filesystem_buttons($hiddens, \@slice_status, $slice); + } + print ui_buttons_row( + 'delete_slice.cgi', + $text{'slice_delete'}, + $text{'slice_deletedesc'}, + ui_hidden("device", $in{'device'}) . "\n" . ui_hidden("slice", $in{'slice'}) + ); + print ui_buttons_end(); +} +# SMART button (physical device) +if (&has_command("smartctl")) { + print ui_hr(); + print ui_buttons_start(); + print ui_buttons_row("smart.cgi", $text{'disk_smart'}, $text{'disk_smartdesc'}, + ui_hidden("device", $disk->{'device'})); + print ui_buttons_end(); +} -if ($canedit) { - print &ui_hr(); - print &ui_buttons_start(); - - if (!@{$slice->{'parts'}}) { - &show_filesystem_buttons($hiddens, \@st, $slice); - } - - # Button to delete slice - print &ui_buttons_row( - 'delete_slice.cgi', - $text{'slice_delete'}, - $text{'slice_deletedesc'}, - &ui_hidden("device", $in{'device'})."\n". - &ui_hidden("slice", $in{'slice'})); - - print &ui_buttons_end(); - } - - -&ui_print_footer("edit_disk.cgi?device=$in{'device'}", - $text{'disk_return'}); +ui_print_footer("edit_disk.cgi?device=$in{'device'}", $text{'disk_return'}); \ No newline at end of file diff --git a/bsdfdisk/fsck.cgi b/bsdfdisk/fsck.cgi index b4f6a8b78..13a1dc737 100755 --- a/bsdfdisk/fsck.cgi +++ b/bsdfdisk/fsck.cgi @@ -27,28 +27,54 @@ else { $object = $slice; } +# Safety checks: do not run fsck on boot partitions or in-use devices +if (is_boot_partition($object)) { + &error($in{'part'} ne '' ? $text{'part_eboot'} : $text{'slice_eboot'}); +} +my @st_obj = &fdisk::device_status($object->{'device'}); +my $use_obj = &fdisk::device_status_link(@st_obj); +if (@st_obj && $st_obj[2]) { + &error(&text('part_esave', $use_obj)); +} + &ui_print_unbuffered_header($object->{'desc'}, $text{'fsck_title'}, ""); -# Do the creation -print &text('fsck_checking', "$object->{'device'}"),"\n"; -print "
\n"; -my $cmd = &get_check_filesystem_command($disk, $slice, $part); -&additional_log('exec', undef, $cmd); -my $fh = "CMD"; -&open_execute_command($fh, $cmd, 2); -while(<$fh>) { - print &html_escape($_); - } -close($fh); -print "
\n"; - } -else { - print $text{'fsck_done'},"
\n"; - } +# If device is ZFS, do not run fsck; show zpool status instead +my $zmap = get_all_zfs_info(); +if ($zmap->{$object->{'device'}}) { + my $pool = $zmap->{$object->{'device'}}; $pool =~ s/^.*?\b([A-Za-z0-9_\-]+)\b.*$/$1/; + print &text('fsck_checking', "$object->{'device'}"),"\n"; + print "
\n"; + my $cmd = "zpool status 2>&1"; + &additional_log('exec', undef, $cmd); + print &html_escape(&backquote_command($cmd)); + print "
\n"; +} else { + # Do the creation + print &text('fsck_checking', "$object->{'device'}"),"\n"; + print "
\n"; + my $cmd = &get_check_filesystem_command($disk, $slice, $part); + &additional_log('exec', undef, $cmd); + my $fh; + &open_execute_command($fh, $cmd, 2); + if ($fh) { + while (my $line = <$fh>) { + $line =~ s/[^\x09\x0A\x0D\x20-\x7E]//g; + print &html_escape($line); + } + close($fh); + } + print "
\n"; + } + else { + print $text{'fsck_done'},"
\n"; + } +} &webmin_log("fsck", $in{'part'} ne '' ? "part" : "object", - $object->{'device'}, $object); + $object->{'device'}, $object); if ($in{'part'} ne '') { &ui_print_footer("edit_part.cgi?device=$in{'device'}&". @@ -59,4 +85,4 @@ else { &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". "slice=$in{'slice'}", $text{'slice_return'}); - } + } \ No newline at end of file diff --git a/bsdfdisk/images/free.gif b/bsdfdisk/images/free.gif new file mode 100644 index 000000000..df1df5e0d Binary files /dev/null and b/bsdfdisk/images/free.gif differ diff --git a/bsdfdisk/index.cgi b/bsdfdisk/index.cgi index 589e62ee2..2964b9099 100755 --- a/bsdfdisk/index.cgi +++ b/bsdfdisk/index.cgi @@ -1,41 +1,57 @@ #!/usr/local/bin/perl -# Show a list of disks - use strict; use warnings; -no warnings 'redefine'; -no warnings 'uninitialized'; require './bsdfdisk-lib.pl'; our (%in, %text, %config, $module_name); -&ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0, - &help_search_link("fdisk", "man")); - -my $err = &check_fdisk(); +# Check prerequisites first +my $err = check_fdisk(); if ($err) { - &ui_print_endpage(&text('index_problem', $err)); - } + ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0); + print "$text{'index_problem'}\n$err\n"; + ui_print_footer("/", $text{'index_return'}); + exit; +} + +# Print header with help link +ui_print_header(undef, $text{'index_title'}, "", "intro", 1, 1, 0, + help_search_link("fdisk", "man")); + +# List and sort disks by device name +my @disks = list_disks_partitions(); +@disks = sort { ($a->{'device'}//'') cmp ($b->{'device'}//'') } @disks; -my @disks = &list_disks_partitions(); -@disks = sort { $a->{'device'} cmp $b->{'device'} } @disks; if (@disks) { - print &ui_columns_start([ $text{'index_dname'}, - $text{'index_dsize'}, - $text{'index_dmodel'}, - $text{'index_dparts'} ]); - foreach my $d (@disks) { - print &ui_columns_row([ - "".&partition_description($d->{'device'})."", - &nice_size($d->{'size'}), - $d->{'model'}, - scalar(@{$d->{'slices'}}), - ]); - } - print &ui_columns_end(); - } -else { - print "$text{'index_none'}
\n"; - } + print ui_columns_start([ + $text{'index_dname'}, + $text{'index_dsize'}, + $text{'index_dmodel'}, + $text{'index_dparts'} + ]); -&ui_print_footer("/", $text{'index'}); + foreach my $d (@disks) { + my $device = $d->{'device'} // ''; + my $disk_name = $device; $disk_name =~ s{^/dev/}{}; + # Prefer mediasize from gpart list (bytes); fallback to diskinfo size + my $base = $device; $base =~ s{^/dev/}{}; + my $ds = get_disk_structure($base); + my $bytes = $ds && $ds->{'mediasize'} ? $ds->{'mediasize'} : $d->{'size'}; + my $size_display = defined $bytes ? safe_nice_size($bytes) : '-'; + my $model = $d->{'model'} // '-'; + my $url_device = urlize($device); + my $slices_cnt = scalar(@{ $d->{'slices'} || [] }); + + print ui_columns_row([ + "$disk_name", + $size_display, + $model, # Now correctly populated from bsdfdisk-lib.pl + $slices_cnt, + ]); + } + print ui_columns_end(); +} +else { + print "$text{'index_none'}
\n"; +} + +ui_print_footer("/", $text{'index_return'}); \ No newline at end of file diff --git a/bsdfdisk/lang/en b/bsdfdisk/lang/en index 3583db17c..01d7be0d2 100644 --- a/bsdfdisk/lang/en +++ b/bsdfdisk/lang/en @@ -1,42 +1,92 @@ index_title=Partitions on Local Disks -index_ecmd=The required command $1 is missing -index_problem=This module cannot be used : $1 -index_none=No disks were found on this system! +index_ecmd=Missing required command $1 +index_problem=Cannot use this module : $1 +index_none=No disks found on this system! index_dname=Disk name index_dsize=Total size index_dmodel=Make and model index_dparts=Slices index_return=list of disks +index_format=Format +index_efdisk=You must have $1 or $2 installed to use this module +disk_edevice=Invalid device parameter +nslice_etype=Invalid slice type +# Disk overview disk_title=Edit Disk disk_egone=Disk no longer exists! -disk_no=Slice disk_type=Type disk_extent=Extent disk_start=Start block disk_end=End block -disk_use=Used by -disk_scount=$1 partitions disk_parts=Partitions disk_free=Free space -disk_vm=Virtual memory -disk_iscsi=iSCSI shared device $1 disk_none=This disk has no slices yet. -disk_size=Size -disk_dsize=Disk size: $1 -disk_model=Make and model: $1 -disk_cylinders=Cylinders: $1 -disk_blocks=Blocks: $1 -disk_device=Device file: $1 +disk_dsize=Disk size +disk_model=Make and model +disk_cylinders=Cylinders +disk_blocks=Blocks +disk_device=Device file disk_return=disk details and list of slices disk_add=Create a new slice. disk_smart=Show SMART Status disk_smartdesc=Show the current status of this drive as detected by SMART, and check it for disk errors. +disk_no=No. +disk_partno=Part. No. +disk_partname=Part. Name +disk_partlabel=Part. Label +disk_subpart=Sub-part. +disk_size=Size +disk_free_space=Free Space +disk_available=Available +disk_format=Format +disk_use=Used by +disk_role=Role Type -select_device=$1 device $2 -select_slice=$1 device $2 slice $3 -select_part=$1 device $2 slice $3 partition $4 +# Debug and scheme +disk_show_debug=Show raw debug information +disk_hide_debug=Hide debug information +disk_scheme=Partition Scheme +disk_unknown=Unknown +disk_sectorsize=Sector Size +disk_bytes=bytes +# GEOM information +disk_geom_header=GEOM Information +disk_geom_details=GEOM Disk Details +disk_mediasize=Media Size +disk_stripesize=Stripe Size +disk_stripeoffset=Stripe Offset +disk_mode=Mode +disk_rotationrate=Rotation Rate +disk_ident=Identifier +disk_lunid=LUN ID +disk_descr=Description +disk_rpm=RPM +disk_ssd=SSD (Solid State Drive) + +# Debug panels +disk_debug_gpart=GPART Output +disk_debug_structure=Parsed Disk Structure +disk_debug_geom=GEOM Disk List +disk_debug_zfs=ZFS Status +disk_debug_zfs_cache=ZFS Devices Cache + +# Roles and usage +disk_inzfs=In ZFS pool +disk_zfs_log=ZFS Log Device +disk_zfs_cache=ZFS Cache Device +disk_zfs_spare=ZFS Spare Device +disk_zfs_mirror=ZFS Mirror +disk_zfs_stripe=ZFS Stripe +disk_zfs_single=ZFS Single Device +disk_zfs_data=ZFS Data Device +disk_boot=Boot Partition +disk_boot_role=System Boot +disk_swap=Swap +disk_swap_role=Virtual Memory + +# Slice pages slice_title=Edit Slice slice_egone=Selected slice does not exist! slice_ssize=Slice size @@ -56,55 +106,70 @@ slice_use=Used by slice_none=This slice has no partitions yet. slice_none2=This slice has no partitions, and none can be created as it is in use as a filesystem. slice_delete=Delete Slice -slice_deletedesc=Delete this slice and all partitions and filesystems within it. Any data on those filesystem will be almost certainly unrecoverable. +slice_deletedesc=Delete this slice and all partitions and filesystems within it. Any data on that filesystem will be almost certainly unrecoverable. slice_return=slice details and list of partitions slice_err=Failed to modify slice slice_header=Slice details slice_suse=Directly used by +slice_adddesc=Create a new partition within this slice. +slice_cannotedit=This slice cannot be modified as it is currently in use. +slice_eboot=Cannot delete this slice as it contains boot partitions +slice_bootdesc=This slice contains bootloader code or kernel files needed to start the system +slice_role=Role +# Delete slice dialogs dslice_title=Delete Slice -dslice_rusure=Are you sure you want to delete the slice $1? Any partitions and filesystems within it will also be deleted. +dslice_rusure=Are you sure you want to delete the slice $1 ? Any partitions and filesystems within it will also be deleted. dslice_warn=Warning - this slice is currently used by : $1 dslice_confirm=Delete Now dslice_deleting=Deleting slice $1 .. dslice_failed=.. deletion failed : $1 dslice_done=.. done +# Create slice nslice_title=Create Slice nslice_header=New slice details nslice_number=Slice number +nslice_autonext=Will auto-select next index nslice_diskblocks=Disk size in blocks nslice_start=Starting block nslice_end=Ending block -nslice_type=New slice type -nslice_makepart=Create default partition? nslice_err=Failed to create slice -nslice_enumber=Missing or non-numeric slice number +nslice_enumber=Slice number must be a number nslice_eclash=A slice with number $1 already exists nslice_estart=Starting block must be a number nslice_eend=Ending block must be a number -nslice_erange=Starting block must be lower than the ending block -nslice_emax=Ending block cannot be larger than the disk size of $1 blocks +nslice_erange=Starting block must be before ending block +nslice_emax=Ending block cannot be larger than disk size of $1 blocks nslice_creating=Creating slice $1 on $2 .. -nslice_failed=.. slice creation failed : $1 +nslice_failed=.. creation failed : $1 nslice_done=.. slice added -nslice_parting=Create default partitions in slice $1 on $2 .. nslice_pfailed=.. partition creation failed : $1 nslice_pdone=.. partition added +nslice_existing_header=Existing slices on this disk +nslice_existing_parts=Existing partitions on this disk +nslice_enospace=No space on device left to create a slice! +epart_existing=Existing partitions on this slice +# Create partition npart_title=Create Partition npart_header=New partition details npart_letter=Partition letter npart_diskblocks=Slice size in blocks +npart_slicerel=(slice-relative) +npart_creserved=partition 'c' is reserved npart_type=Partition type npart_err=Failed to create partition -npart_eletter=Partition number must be a letter from A to D +npart_eletter=Partition letter must be a-h (excluding c) +npart_ereserved=Partition 'c' is reserved for the whole slice in BSD disklabels npart_eclash=A partition with letter $1 already exists -npart_emax=Ending block cannot be larger than the slice size of $1 blocks +npart_emax=Ending block cannot be larger than slice size of $1 blocks +npart_erange=Start block must be less than end block npart_creating=Creating partition $1 on $2 .. npart_failed=.. partition creation failed : $1 npart_done=.. partition added +# Edit partition part_title=Edit Partition part_egone=Partition no longer exists! part_header=Partition details @@ -128,18 +193,29 @@ part_err=Failed to save partition part_esave=Currently in use by $1 part_newmount=Mount Partition On: part_newmount2=Mount Partition -part_mountmsg=Mount this device on new directory on your system, so that it can be used to store files. A filesystem must have been already created on the device. -part_mountmsg2=Mount this device as virtual memory on your system, to increase the amount of memory available. +part_mountmsg=Mount this device on a new directory on your system, so that it can be used to store files. A filesystem must already be created on the device. +part_mountmsg2=Mount this device as virtual memory on your system to increase the amount of available memory. part_cannotedit=This partition cannot be modified as it is currently in use. +part_boot=Boot Partition +part_eboot=Cannot delete boot partitions as this may render the system unbootable +part_bootdesc=This partition contains bootloader code or kernel files needed to start the system +part_zfslog=ZFS Log Device +part_zfsdata=ZFS Data Device +part_mounted=Mounted on $1 +part_unused=Not in use +part_bootcannotedit=This partition cannot be modified as it is a boot partition. Changing it could render the system unbootable. +part_role=Role +# Delete partition dialogs dpart_title=Delete Partition -dpart_rusure=Are you sure you want to delete the partition $1? Any filesystems within it will also be deleted. +dpart_rusure=Are you sure you want to delete the partition $1 ? Any filesystems in it will also be deleted. dpart_warn=Warning - this partition is currently used by $1 dpart_confirm=Delete Now dpart_deleting=Deleting partition $1 .. dpart_failed=.. deletion failed : $1 dpart_done=.. done +# New filesystem newfs_title=Create Filesystem newfs_header=New filesystem details newfs_free=Space to reserve for root @@ -147,27 +223,36 @@ newfs_deffree=Default (8%) newfs_trim=Enable TRIM mode for SSDs newfs_label=Filesystem label newfs_none=None -newfs_create=Create Now newfs_err=Failed to create filesystem -newfs_efree=Space to reserve for root must be a percentage -newfs_elabel=Missing or invalid label +newfs_efree=Missing or invalid percentage of free space +newfs_elabel=Missing or invalid filesystem label newfs_creating=Creating filesystem on $1 .. -newfs_failed=.. creation failed! -newfs_done=.. created successfully +newfs_failed=.. creation failed : $1 +newfs_done=.. filesystem created +# FS check fsck_title=Check Filesystem +fsck_header=Filesystem check options +fsck_repair=Repair mode +fsck_fix=Only fix safe errors +fsck_fix2=Try to fix all errors fsck_err=Failed to check filesystem -fsck_checking=Checking filesystem on $1 .. +fsck_exec=Executing command $1 .. fsck_failed=.. check failed! -fsck_done=.. check completed with no errors found +fsck_done=.. check complete +fsck_checking=Checking filesystem on $1 .. -log_create_slice=Created slice $1 -log_delete_slice=Deleted slice $1 -log_modify_slice=Modified slice $1 -log_create_part=Created partition $1 -log_delete_part=Deleted partition $1 -log_modify_part=Modified partition $1 -log_newfs_part=Created filesystem on partition $1 -log_fsck_part=Checked filesystem on partition $1 +# Logging +action_create_slice=Created slice $1 +action_delete_slice=Deleted slice $1 +action_modify_slice=Modified slice $1 +action_create_part=Created partition $1 +action_delete_part=Deleted partition $1 +action_modify_part=Modified partition $1 +action_create_fs=Created filesystem on $1 +action_check_fs=Checked filesystem on $1 -__norefs=1 +# Generic +save=Save +yes=Yes +no=No diff --git a/bsdfdisk/newfs.cgi b/bsdfdisk/newfs.cgi index 384030035..e97cee1e5 100755 --- a/bsdfdisk/newfs.cgi +++ b/bsdfdisk/newfs.cgi @@ -27,13 +27,25 @@ else { $object = $slice; } +# Safety checks: do not run newfs on boot partitions or in-use devices +if (is_boot_partition($object)) { + &error($in{'part'} ne '' ? $text{'part_eboot'} : $text{'slice_eboot'}); +} +my @st_obj = &fdisk::device_status($object->{'device'}); +my $use_obj = &fdisk::device_status_link(@st_obj); +if (@st_obj && $st_obj[2]) { + &error(&text('part_esave', $use_obj)); +} + # Validate inputs my $newfs = { }; -$in{'free_def'} || $in{'free'} =~ /^\d+$/ && $in{'free'} <= 100 || +$in{'free_def'} || + ($in{'free'} =~ /^\d+$/ && $in{'free'} >= 0 && $in{'free'} <= 100) || &error($text{'newfs_efree'}); $newfs->{'free'} = $in{'free_def'} ? undef : $in{'free'}; $newfs->{'trim'} = $in{'trim'}; -$in{'label_def'} || $in{'label'} =~ /^\S+$/ || +$in{'label_def'} || + length($in{'label'}) > 0 || &error($text{'newfs_elabel'}); $newfs->{'label'} = $in{'label_def'} ? undef : $in{'label'}; @@ -44,12 +56,14 @@ print &text('newfs_creating', "$object->{'device'}"),"\n"; print "
\n"; my $cmd = &get_create_filesystem_command($disk, $slice, $part, $newfs); &additional_log('exec', undef, $cmd); -my $fh = "CMD"; +my $fh; &open_execute_command($fh, $cmd, 2); -while(<$fh>) { - print &html_escape($_); - } -close($fh); +if ($fh) { + while(<$fh>) { + print &html_escape($_); + } + close($fh); +} print "
\n"; @@ -58,6 +72,18 @@ else { print $text{'newfs_done'},"
\n"; &webmin_log("newfs", $in{'part'} ne '' ? "part" : "object", $object->{'device'}, $object); + # If a label was provided, set the partition label (GPT slice or BSD sub-partition) + if (!$in{'label_def'} && defined $in{'label'} && length $in{'label'}) { + my $errlbl = set_partition_label( + disk => $disk, + slice => $slice, + part => ($in{'part'} ne '' ? $part : undef), + label => $in{'label'} + ); + if ($errlbl) { + print "Warning: failed to set partition label: \n" . &html_escape($errlbl) . "\n"; + } + } } if ($in{'part'} ne '') { @@ -69,4 +95,4 @@ else { &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". "slice=$in{'slice'}", $text{'slice_return'}); - } + } \ No newline at end of file diff --git a/bsdfdisk/newfs_form.cgi b/bsdfdisk/newfs_form.cgi index 7466a2448..5bff91f0d 100755 --- a/bsdfdisk/newfs_form.cgi +++ b/bsdfdisk/newfs_form.cgi @@ -10,21 +10,27 @@ our (%in, %text, $module_name); &ReadParse(); # Get the disk and slice -my @disks = &list_disks_partitions(); -my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; -$disk || &error($text{'disk_egone'}); -my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; -$slice || &error($text{'slice_egone'}); -my $object; -if ($in{'part'} ne '') { - my ($part) = grep { $_->{'letter'} eq $in{'part'} } - @{$slice->{'parts'}}; - $part || &error($text{'part_egone'}); - $object = $part; - } -else { - $object = $slice; - } + # Validate input parameters to prevent command injection + $in{'device'} =~ /^[a-zA-Z0-9_\/.-]+$/ or &error("Invalid device name"); + $in{'device'} !~ /\.\./ or &error("Invalid device name"); + $in{'slice'} =~ /^\d+$/ or &error("Invalid slice number") if $in{'slice'}; + $in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter") if $in{'part'}; + my @disks = &list_disks_partitions(); + my ($disk) = grep { $_->{'device'} eq $in{'device'} } @disks; + $disk || &error($text{'disk_egone'}); + my ($slice) = grep { $_->{'number'} eq $in{'slice'} } @{$disk->{'slices'}}; + $slice || &error($text{'slice_egone'}); + my $object; + if ($in{'part'} ne '') { + $in{'part'} =~ /^[a-z]$/ or &error("Invalid partition letter"); + my ($part) = grep { $_->{'letter'} eq $in{'part'} } + @{$slice->{'parts'}}; + $part || &error($text{'part_egone'}); + $object = $part; + } + else { + $object = $slice; + } &ui_print_header($object->{'desc'}, $text{'newfs_title'}, ""); @@ -38,24 +44,24 @@ print &ui_table_row($text{'part_device'}, "$object->{'device'}"); print &ui_table_row($text{'newfs_free'}, - &ui_opt_textbox("free", undef, 4, $text{'newfs_deffree'})."%"); + &ui_opt_textbox("free", undef, 4, $text{'newfs_deffree'}) . "%"); print &ui_table_row($text{'newfs_trim'}, - &ui_yesno_radio("trim", 0)); + &ui_yesno_radio("trim", 0)); print &ui_table_row($text{'newfs_label'}, - &ui_opt_textbox("label", undef, 20, $text{'newfs_none'})); + &ui_opt_textbox("label", undef, 20, $text{'newfs_none'})); print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'newfs_create'} ] ]); +print &ui_form_end([ [ undef, $text{'save'} ] ]); if ($in{'part'} ne '') { - &ui_print_footer("edit_part.cgi?device=$in{'device'}&". - "slice=$in{'slice'}&part=$in{'part'}", - $text{'part_return'}); - } + &ui_print_footer("edit_part.cgi?device=$in{'device'}&" . + "slice=$in{'slice'}&part=$in{'part'}", + $text{'part_return'}); + } else { - &ui_print_footer("edit_slice.cgi?device=$in{'device'}&". - "slice=$in{'slice'}", - $text{'slice_return'}); - } + &ui_print_footer("edit_slice.cgi?device=$in{'device'}&" . + "slice=$in{'slice'}", + $text{'slice_return'}); + } \ No newline at end of file diff --git a/bsdfdisk/part_form.cgi b/bsdfdisk/part_form.cgi index f6eb6fefe..57a8661c3 100755 --- a/bsdfdisk/part_form.cgi +++ b/bsdfdisk/part_form.cgi @@ -23,37 +23,65 @@ print &ui_hidden("device", $in{'device'}); print &ui_hidden("slice", $in{'slice'}); print &ui_table_start($text{'npart_header'}, undef, 2); -# Partition number (first free) +# Partition number (first free, skipping 'c' which is reserved for the whole slice) my %used = map { $_->{'letter'}, $_ } @{$slice->{'parts'}}; +$used{'c'} = 1; # Reserve 'c' for the whole slice (BSD convention) my $l = 'a'; while($used{$l}) { $l++; } print &ui_table_row($text{'npart_letter'}, - &ui_textbox("letter", $l, 4)); + &ui_textbox("letter", $l, 4) . " (" . $text{'npart_creserved'} . ")"); # Slice size in blocks print &ui_table_row($text{'npart_diskblocks'}, $slice->{'blocks'}); -# Start and end blocks (defaults to last part) -my ($start, $end) = (0, $slice->{'blocks'}); -foreach my $p (sort { $a->{'startblock'} cmp $b->{'startblock'} } - @{$slice->{'parts'}}) { - $start = $p->{'startblock'} + $p->{'blocks'} + 1; - } -print &ui_table_row($text{'nslice_start'}, - &ui_textbox("start", $start, 10)); -print &ui_table_row($text{'nslice_end'}, - &ui_textbox("end", $end, 10)); - +# Start and end blocks for BSD partitions are SLICE-RELATIVE (not disk-absolute) +# Start at 0 (or after last partition), end at slice size - 1 +my ($start, $end) = (0, $slice->{'blocks'} - 1); +foreach my $p (sort { $a->{'startblock'} <=> $b->{'startblock'} } + @{$slice->{'parts'}}) { + # Partitions are already stored as slice-relative + $start = $p->{'startblock'} + $p->{'blocks'}; +} +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'} . " " . $text{'npart_slicerel'}, + &ui_textbox("start", $start, 10)); +print &ui_table_row($text{'nslice_end'} . " " . $text{'npart_slicerel'}, + &ui_textbox("end", $end, 10)); + # Partition type +# For BSD-on-MBR inner label partitions, offer FreeBSD partition types +my $scheme = 'BSD'; +my $default_ptype = 'freebsd-ufs'; print &ui_table_row($text{'npart_type'}, - &ui_select("type", '4.2BSD', - [ &list_partition_types() ])); - + &ui_select("type", $default_ptype, + [ list_partition_types($scheme) ])); + print &ui_table_end(); -print &ui_form_end([ [ undef, $text{'create'} ] ]); +print &ui_form_end([ [ undef, $text{'save'} ] ]); +# Existing partitions summary +if (@{$slice->{'parts'}||[]}) { + my $zfs = get_all_zfs_info(); + print &ui_hr(); + print &ui_columns_start([ + $text{'slice_letter'}, $text{'slice_type'}, $text{'slice_start'}, $text{'slice_end'}, $text{'slice_size'}, $text{'slice_use'}, $text{'slice_role'} + ], $text{'epart_existing'}); + foreach my $p (sort { $a->{'startblock'} <=> $b->{'startblock'} } @{$slice->{'parts'}}) { + my $ptype = get_type_description($p->{'type'}) || $p->{'type'}; + my @stp = fdisk::device_status($p->{'device'}); + my $usep = $zfs->{$p->{'device'}} || fdisk::device_status_link(@stp) || $text{'part_nouse'}; + my $rolep = get_partition_role($p); + my $pb = bytes_from_blocks($p->{'device'}, $p->{'blocks'}); + print &ui_columns_row([ + uc($p->{'letter'}), $ptype, $p->{'startblock'}, $p->{'startblock'} + $p->{'blocks'} - 1, ($pb ? safe_nice_size($pb) : '-'), $usep, $rolep + ]); + } + print &ui_columns_end(); +} + &ui_print_footer("edit_slice.cgi?device=$in{'device'}&slice=$in{'slice'}", - $text{'slice_return'}); + $text{'slice_return'}); \ No newline at end of file diff --git a/bsdfdisk/save_slice.cgi b/bsdfdisk/save_slice.cgi index 69cbda19f..22c2540ef 100755 --- a/bsdfdisk/save_slice.cgi +++ b/bsdfdisk/save_slice.cgi @@ -20,9 +20,22 @@ $slice || &error($text{'slice_egone'}); # Apply changes my $oldslice = { %$slice }; $slice->{'type'} = $in{'type'}; -if (!$slice->{'active'}) { - $slice->{'active'} = $in{'active'}; - } +$slice->{'active'} = $in{'active'} if (defined $in{'active'}); + +# Apply active flag for MBR disks via gpart set/unset when it changed +my $base = $disk->{'device'}; $base =~ s{^/dev/}{}; +my $ds = get_disk_structure($base); +if (is_using_gpart() && $ds && $ds->{'scheme'} && $ds->{'scheme'} !~ /GPT/i) { + my $idx = slice_number($slice); + if (defined $oldslice->{'active'} && defined $slice->{'active'} && $oldslice->{'active'} != $slice->{'active'}) { + my $cmd = $slice->{'active'} ? "gpart set -a active -i $idx $base" : "gpart unset -a active -i $idx $base"; + my $out = `$cmd 2>&1`; + if ($? != 0) { + &error("Failed to change active flag: $out"); + } + } +} + my $err = &modify_slice($disk, $oldslice, $slice); &error($err) if ($err); diff --git a/bsdfdisk/slice_form.cgi b/bsdfdisk/slice_form.cgi index 904310e43..14a6d917c 100755 --- a/bsdfdisk/slice_form.cgi +++ b/bsdfdisk/slice_form.cgi @@ -16,6 +16,29 @@ $disk || &error($text{'disk_egone'}); &ui_print_header($disk->{'desc'}, $text{'nslice_title'}, ""); +# Determine scheme for read-only behavior and note +my $base_device = $disk->{'device'}; $base_device =~ s{^/dev/}{}; +my $disk_structure = get_disk_structure($base_device); +my $is_gpt = (is_using_gpart() && $disk_structure && ($disk_structure->{'scheme'}||'') =~ /GPT/i); + +# Check if there is any free space on the device +my $has_free_space = 0; +if ($disk_structure && $disk_structure->{'entries'}) { + foreach my $entry (@{$disk_structure->{'entries'}}) { + if ($entry->{'type'} eq 'free' && $entry->{'size'} > 0) { + $has_free_space = 1; + last; + } + } +} + +# If no free space, show error and return +if (!$has_free_space) { + print "
$text{'nslice_enospace'}
" . &html_escape("Command: $cmd\n\n$out") . "