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