# vim: ft=zsh noet ts=4 sts=4 sw=4
#
# confz functions for dealing with filesystem and mounting
#
# load the zstat builtin and keep stat external
zmodload -F zsh/stat b:zstat
# helper for finding disk drives by their metadata
fs_smartctl_probe() {
(($+smartctl_probed)) && (($smartctl_probed)) && return 0
local dev item val rest
typeset -g smartctl_probed
typeset -ga smartctl_devices
typeset -gA smartctl_info
smartctl --scan | while read dev rest; do
smartctl_devices+=( $dev )
smartctl -i $dev | while IFS=':' read item val; do smartctl_info[${dev}:${${item:l}// /_}]=${val/# #}; done
done
smartctl_probed=1
}
# helper for probing devices
fs_blkid_probe() {
unset fs_blkid_output fs_blkid_result
typeset -gA fs_blkid_output
typeset -g fs_blkid_result
local out key val
fs_blkid_output=( )
[[ -b $1 ]] || die "no such block device: ${(qqq)1}"
out=$( blkid -o export -p $1 )
fs_blkid_result=$?
case $fs_blkid_result in
(2);;
(0) while IFS='=' read key val; do
[[ $key == *' '* ]] && die "Malformed blkid output for \"export\" format."
fs_blkid_output[$key]=$val
done <<<$out;;
(*) die "blkid probe failed on device: ${(qqq)1}"
esac
}
# Check that given device is formatted with given type (return 0) or blank (return 1), die otherwise
fs_check_type() {
local device fstype kind
device=$1
fstype=$2
kind=${3:-TYPE} # TYPE = filesystem type, PTTYPE = partition format
fs_blkid_probe $device
if (($fs_blkid_result)); then
# empty label
return 1
elif (( $+fs_blkid_output[$kind] )); then
if [[ ${fs_blkid_output[$kind]:-} == $fstype ]]; then
return 0
else
die "$0: non-$fstype label (${fs_blkid_output[$kind]}) already present on device: ${(qqq)device}"
fi
else
local -a labels
local k v
for k v in "${(kv)fs_blkid_output[@]}"; do
# Filter out the information about partition layout rather than content
case $k in
(DEVNAME) continue;;
(PART_ENTRY_*) continue;;
(*) labels+=( "$k=${(qqq)v}" )
esac
done
if (( $#labels )); then
die "$0: unexpected filesystem labels present on device: ${(qqq)device} $labels[*]"
fi
# empty label
return 1
fi
}
# helper for listing partition table
fs_parted_list() {
unset fs_parted_start fs_parted_end fs_parted_size fs_parted_type
unset fs_parted_filesystem fs_parted_name fs_parted_flags
typeset -gA fs_parted_start fs_parted_end fs_parted_size fs_parted_type
typeset -gA fs_parted_filesystem fs_parted_name fs_parted_flags
local line got_header number out
local NumberE StartB StartE EndB EndE SizeB SizeE FSB FSE NameB NameE FlagsB
local TypeB TypeE
local MATCH MBEGIN MEND
[[ -b $1 ]] || die "no such block device: ${(qqq)1}"
out=$( parted --script $1 -- unit s print ) || \
die "parted print of device failed: ${(qqq)1}"
got_header=0
while IFS= read line; do
if ((got_header)); then
number=${${line[1,$NumberE]}// }
fs_parted_start[$number]=${${line[$StartB,$StartE]}// }
fs_parted_end[$number]=${${line[$EndB,$EndE]}// }
fs_parted_size[$number]=${${line[$SizeB,$SizeE]}// }
fs_parted_flags[$number]=${line[$FlagsB,-1]}
[[ -n $TypeB ]] && \
fs_parted_type[$number]=${${line[$TypeB,$TypeE]}// }
[[ -n $NameB ]] && \
fs_parted_name[$number]=${${line[$NameB,$NameE]}// }
[[ -n $FSB ]] && \
fs_parted_filesystem[$number]=${${line[$FSB,$FSE]}// }
elif [[ $line == Number* ]]; then
got_header=1
[[ $line =~ 'Number +' ]] || die "malformed header: ${(qqq)line}"
NumberE=$MEND
[[ $line =~ 'Start +' ]] || die "malformed header: ${(qqq)line}"
StartB=$MBEGIN
StartE=$MEND
[[ $line =~ 'End +' ]] || die "malformed header: ${(qqq)line}"
EndB=$MBEGIN
EndE=$MEND
[[ $line =~ 'Size +' ]] || die "malformed header: ${(qqq)line}"
SizeB=$MBEGIN
SizeE=$MEND
[[ $line =~ 'Flags *' ]] || die "malformed header: ${(qqq)line}"
FlagsB=$MBEGIN
if [[ $line =~ 'Type +' ]]; then
TypeB=$MBEGIN
TypeE=$MEND
fi
if [[ $line =~ 'Name +' ]]; then
NameB=$MBEGIN
NameE=$MEND
fi
if [[ $line =~ 'File system +' ]]; then
FSB=$MBEGIN
FSE=$MEND
fi
fi
done <<<"$out"
}
# Find disks by their IDs
confz_disk_id_check() {
# arguments: device device_model serial_number
if ! (($+vars[device] + $+vars[device_model] + $+vars[serial_number])); then
die "$0: requires one or more arguments: device device_model serial_number"
fi
do_command=( true )
fs_smartctl_probe
local device
local -a found
for device in $smartctl_devices; do
if (($+vars[device])); then
[[ $vars[device] != $device ]] && continue
fi
if (($+vars[device_model])); then
[[ $vars[device_model] != $smartctl_info[${device}:device_model] ]] && continue
fi
if (($+vars[serial_number])); then
[[ $vars[serial_number] != $smartctl_info[${device}:serial_number] ]] && continue
fi
found+=( $device $smartctl_info[${device}:device_model] $smartctl_info[${device}:serial_number] )
done
(( $#found )) || die "$0: found no matching devices"
(( $#found % 3 )) && die "$0: internal error parsing device information"
[[ $#found -ne 3 ]] && die "$0: found too many matching devices"
vars[device]=$found[1]
vars[device_model]=$found[2]
vars[serial_number]=$found[3]
}
# check for DOS-style MBR on device
confz_disklabel_dos_check() {
checkvars device
do_command=( parted --script $vars[device] -- mklabel msdos )
fs_check_type $vars[device] dos PTTYPE
}
# check for GPT partition table on device
confz_disklabel_gpt_check() {
checkvars device
do_command=( parted --script $vars[device] -- mklabel gpt )
fs_check_type $vars[device] gpt PTTYPE
}
# embed file in MBR
confz_mbr_code_check() {
checkvars device from
do_command=( dd bs=440 count=1 if=$vars[from] of=$vars[device] )
dd if=$vars[device] bs=440 count=1 | cmp -s - $vars[from]
}
# check for gpt partition
confz_gpt_partition_check() {
local prev option
checkvars device number name
defvar fs ext2
require disklabel_gpt :device
fs_parted_list $vars[device]
(( $+vars[size] || $+vars[end] )) || die "$0: requires either size or end"
if [[ $vars[number] != 1 ]]; then
prev=$[ $vars[number] - 1 ]
(( $+fs_parted_start[$prev] )) || die "$0: missing preceding partition"
: ${vars[start]:=${fs_parted_start[$prev]}}
else
checkvars start
fi
if ! (($+vars[end])); then
vars[end]=$[${${vars[start]}%s} + ${${vars[size]}%s}]s
fi
if (( $+fs_parted_start[$vars[number]] )); then
[[ $fs_parted_name[$vars[number]] == $vars[name] ]] || \
die "Partition $vars[number] has name ${(qqq)fs_parted_name[$vars[number]}, want ${(qqq)vars[name]}"
return 0
else
do_command=( parted --script )
(($+vars[align])) && do_command+=( --align $vars[align] )
do_command+=(
$vars[device] -- unit s
mkpart $vars[name] $vars[fs] $vars[start] $vars[end]
)
(($+vars[align])) && do_command+=(
align-check $vars[align] $vars[number]
)
(($+vars[options])) && for option in ${=vars[options]}; do
do_command+=( set $vars[number] $option on )
done
return 1
fi
}
# check for primary partition
confz_primary_partition_check() {
local prev option
checkvars device number
defvar fs ext2
require disklabel_dos :device
fs_parted_list $vars[device]
(( $+vars[size] || $+vars[end] )) || die "$0: requires either size or end"
if [[ $vars[number] != 1 ]]; then
prev=$[ $vars[number] - 1 ]
(( $+fs_parted_start[$prev] )) || die "$0: missing preceding partition"
: ${vars[start]:=${fs_parted_start[$prev]}}
else
checkvars start
fi
if ! (($+vars[end])); then
vars[end]=$[${${vars[start]}%s} + ${${vars[size]}%s}]s
fi
if (( $+fs_parted_start[$vars[number]] )); then
return 0
else
do_command=( parted --script )
(($+vars[align])) && do_command+=( --align $vars[align] )
do_command+=(
$vars[device] -- unit s
mkpart primary $vars[fs] $vars[start] $vars[end]
)
(($+vars[align])) && do_command+=(
align-check $vars[align] $vars[number]
)
(($+vars[options])) && for option in ${=vars[options]}; do
do_command+=( set $vars[number] $option on )
done
return 1
fi
}
# check for bootable flag on primary partition
confz_bootable_partition_check() {
checkvars device number
require disklabel_dos :device
fs_parted_list $vars[device]
(( $+fs_parted_flags[$vars[number]] )) || \
die "$0: device ${(q)vars[device]} partition ${(q)vars[number]} not found"
if [[ $fs_parted_flags[$vars[number]] = *boot* ]]; then
return 0
else
do_command=( parted --script $vars[device] -- set $vars[number] boot on )
return 1
fi
}
# create swap partition
confz_swap_check() {
checkvars device
do_command=( mkswap $vars[device] )
fs_check_type $vars[device] swap
}
# enable swap device
confz_swapon_check() {
local out device rest
checkvars device
require swap :device
out=$(swapon -s) || die "$0: swapon -s comman failed"
while read device rest; do
[[ $device == $vars[device] ]] && return 0
done <<<$out
do_command=( swapon $vars[device] )
return 1
}
# set up /dev/md* device
confz_mdraid_check() {
local line level out seen_level seen_header devices all_empty ret
local -a seen_devices devices device_numbers
local -A level_map
checkvars md_device raid_devices level
defvar metadata default
defvar bitmap none
devices=( "${(Q@)${(z)vars[raid_devices]}}" )
level_map=(
linear linear
raid0 0
0 0
stripe 0
raid1 1
1 1
mirror 1
raid4 4
4 4
raid5 5
5 5
raid6 6
6 6
raid10 10
10 10
multipath multipath
mp multipath
faulty faulty
container container
)
level=${level_map[${vars[level]}]}
for device in $devices; do
[[ -b $device ]] || die "$0: not a block device: ${(qqq)device}"
device_numbers+=( $(zstat +rdev $device) ) || die "$0: could not stat ${(qqq)device}"
done
if [[ -b $vars[md_device] ]]; then
out=$( mdadm --detail $vars[md_device] )
ret=$?
case $ret in
(0) ;;
(1) ;; # return 1;;
(*) die "$0: mdadm --detail ${(qqq)vars[md_device]} returned $ret";;
esac
seen_header=0
while read line; do
if ! (($seen_header)); then
case $line in
('Raid Level :'*)
seen_level=$level_map[${line#* : }];;
(Number*Major*Minor*RaidDevice*State)
seen_header=1;;
esac
else
seen_devices+=( $(( ${${=line}[2]} << 8 + ${${=line}[3]} )) )
fi
done <<<$out
if (($seen_header)); then
[[ $level == $seen_level ]] || \
die "$0: raid level mismatch." \
"expected: ${(q)level} got: ${(q)seen_level}"
[[ ${(j.:.)${(o)device_numbers}} == ${(j.:.)${(o)seen_devices}} ]] || \
die $0$': raid device mismatch\nexpected:' \
${(oqqq)device_numbers}$'\ngot:' \
${(oqqq)seen_devices}
return 0
fi
fi
all_empty=1
for device in $devices; do
# all devices either need to have empty labels or be linux_raid_member
if fs_check_type $device linux_raid_member; then
all_empty=0
fi
done
if (($all_empty)); then
# empty labels
do_command=(
mdadm --create
-l $level
--metadata=$vars[metadata]
--bitmap=$vars[bitmap]
-n $#devices
$vars[md_device]
$devices
)
else
# already created
do_command=( mdadm --assemble $vars[md_device] $devices )
fi
return 1
}
# set up LVM2 physical volume
confz_physical_volume_check() {
checkvars device
do_command=( lvm pvcreate $vars[device] )
fs_check_type $vars[device] LVM2_member
}
# configure LVM2 volume group
confz_volume_group_check() {
local -A devices volumes
local -a vg_devices
local device
[[ -n ${vars[vg_name]:=${DEFAULT_VG}} ]] || \
die "$0: DEFAULT_VG is unset and no 'vg_name' was passed"
checkvars vg_devices
vg_devices=( "${(Q@)${(z)vars[vg_devices]}}" )
for device in $vg_devices; do
require physical_volume device=$device
done
volumes=( $(lvm vgs --noheadings -o vg_name,pv_count) ) || \
die "$0: lvm vgs command returned error"
if ! (($+volumes[${vars[vg_name]}])); then
do_command=( lvm vgcreate ${vars[vg_name]} $vg_devices )
return 1
fi
devices=( $(lvm pvs --noheadings -o pv_name,vg_name) ) || \
die "$0: lvm pvs command returned error"
do_command=( lvm vgextend ${vars[vg_name]} )
for device in $vg_devices; do
[[ $+devices[$device] == 1 && $devices[$device] == $vars[vg_name] ]] || \
do_command+=( $device )
done
(( $#do_command == 3 ))
}
# configure LVM2 logical volume
confz_logical_volume_check() {
[[ -n ${vars[vg_name]:=${DEFAULT_VG}} ]] || \
die "$0: DEFAULT_VG is unset and no 'vg_name' was passed"
[[ -n ${vars[size]:=${DEFAULT_VOLUME_SIZE}} ]] || \
die "$0: DEFAULT_VOLUME_SIZE is unset and no 'size' was passed"
checkvars lv_name
setvar device /dev/mapper/$vars[vg_name]-$vars[lv_name]
do_command=(
lvcreate
--name ${vars[lv_name]}
--size ${vars[size]}
${vars[vg_name]}
)
fail_reason="not a block device: ${vars[device]}"
[[ -b ${vars[device]} ]]
}
# configure LVM2 logical volume
confz_logical_volume_extents_check() {
[[ -n ${vars[vg_name]:=${DEFAULT_VG}} ]] || \
die "$0: DEFAULT_VG is unset and no 'vg_name' was passed"
checkvars lv_name pv_name extents
local vg_name lv_name pv_name pvseg_start pvseg_size segtype
local -A found
pvs --segments --noheadings --separator $'\t' \
-o vg_name,lv_name,pv_name,pvseg_start,pvseg_size,segtype \
| while IFS=$'\t' \
read vg_name lv_name pv_name pvseg_start pvseg_size segtype
do
vg_name=${vg_name/# #} # strip preceding space
if matchvars vg_name $vg_name lv_name $lv_name; then
(($#found)) && die "$0: more than one segment found"
if [[ $segtype != linear ]] || ! matchvars pv_name $pv_name offset $pvseg_start extents $pvseg_size; then
die "$0: non-matching physical layout of the volume"
fi
found[offset]=$pvseg_start
fi
done
unify "${(kv@)found}" \
device /dev/mapper/$vars[vg_name]-$vars[lv_name]
do_command=(
lvcreate
--name ${vars[lv_name]}
--extents ${vars[extents]}
${vars[vg_name]}
$vars[pv_name]:$vars[offset]-$[ $vars[offset] + $vars[extents] ]
)
if ! (($#found)); then
fail_reason="not found in pvs output: ${vars[vg_name]}/${vars[lv_name]}"
return 1
fi
fail_reason="not a block device: ${vars[device]}"
[[ -b ${vars[device]} ]]
}
# create filesystem on block device
confz_filesystem_check() {
[[ -n ${vars[filesystem]:=${DEFAULT_FS}} ]] || \
die "$0: DEFAULT_FS is unset and no 'filesystem' was passed"
checkvars label device filesystem
defvar mkfs_opts ''
[[ -b ${vars[device]} ]] || \
die "$0: not a block device: ${(qqq)vars[device]}"
do_command=(
mkfs -t ${vars[filesystem]}
)
case $vars[filesystem] in
(xfs|btrfs|ext[234])
do_command+=( -L "${vars[label]}" );;
(reiserfs)
# Asks questions without -q
do_command+=( -l "${vars[label]}" -q );;
(vfat)
do_command+=( -n "${vars[label]}" );;
(*)
if [[ -n $vars[label] ]]; then
die "$0: I don't know how to set label on ${(qqq)vars[filesystem]}"
fi
;;
esac
[[ -n $vars[mkfs_opts] ]] && do_command+=( "${(Q@)${(z)vars[mkfs_opts]}}" )
do_command+=( ${vars[device]} )
local tries # blk_out DEVNAME LABEL UUID TYPE SEC_TYPE PARTLABEL PARTUUID
tries=10
while ((tries)); do
fs_blkid_probe $vars[device]
[[ $fs_blkid_result == 0 ]] && break
tries=$[$tries - 1]
done
fail_reason="no blkid signature found on $vars[device]"
# Check if anything was detected at all
(( $#fs_blkid_output )) || return 1
# Check if it is a filesystem
[[ -z ${fs_blkid_output[TYPE]:-} && -z ${fs_blkid_output[LABEL]:-} && -z ${fs_blkid_output[UUID]:-} ]] && return 1
# Check if it matches requirements
[[ ${fs_blkid_output[LABEL]:-} == ${vars[label]:-} && $fs_blkid_output[TYPE] == $vars[filesystem] ]] && return 0
die "$0: filesystem already present on ${(qqq)vars[device]}!"
}
# put mountpoint for device into /etc/fstab
confz_fstab_check() {
checkvars device mountpoint filesystem
defvar opts noatime
defvar dump 0
defvar fstab /etc/fstab
if [[ $vars[filesystem] == xfs ]]; then
defvar pass 0
else
defvar pass 2
fi
local device mountpoint filesystem opts dump pass
sed '/^[ \t]*#/d;s/#.*//;s/[ \t]\+/ /g' $vars[fstab] | \
while read device mountpoint filesystem opts dump pass; do
if [[ $mountpoint == ${vars[mountpoint]} ]]; then
[[ $device == ${vars[device]} && opts != *bind* ]] || \
die "$0: $mountpoint already present" \
"with different device than ${(qqq)vars[device]}"
[[ $filesystem == ${vars[filesystem]} ]] || \
die "$0: $mountpoint already present" \
"with different filesystem than ${(qqq)vars[filesystem]}"
[[ $opts == ${vars[opts]} ]] || \
die "$0: $mountpoint already present" \
"with different opts than ${(qqq)vars[opts]}"
[[ $dump == ${vars[dump]} ]] || \
die "$0: $mountpoint already present" \
"with different dump than ${(qqq)vars[dump]}"
[[ $pass == ${vars[pass]} ]] || \
die "$0: $mountpoint already present" \
"with different pass than ${(qqq)vars[pass]}"
return 0 # found matching entry
fi
done
fail_reason="no entry for ${(qqq)vars[mountpoint]} in ${(qqq)vars[fstab]}"
return 1 # did not find matching entry
}
confz_fstab_do() {
print -r - >>$vars[fstab] ${vars[device]}$'\t'${vars[mountpoint]}$'\t'${vars[filesystem]}$'\t'${vars[opts]}$'\t'${vars[dump]}' '${vars[pass]}
}
# make device mounted on mountpoint
confz_mounted_check() {
checkvars device mountpoint
fail_reason="could not find ${vars[device]} ${vars[mountpoint]} in /proc/mounts"
grep -q "^${vars[device]} ${vars[mountpoint]} " /proc/mounts
}
confz_mounted_do() {
mkdir -p ${vars[mountpoint]} || return $?
mount "$@" ${vars[device]} ${vars[mountpoint]}
}
# make device mounted on mountpoint
confz_bind_mounted_check() {
checkvars device origin mountpoint
fail_reason="could not find ${vars[device]} ${vars[mountpoint]} in /proc/mounts"
grep -q "^${vars[device]} ${vars[mountpoint]} " /proc/mounts
}
confz_bind_mounted_do() {
mkdir -p ${vars[mountpoint]} || return $?
mount --bind "$@" ${vars[origin]} ${vars[mountpoint]}
}
# create LVM2 logical volume, and make sure it's in fstab and mounted
confz_mounted_volume_check() {
checkvars mountpoint
defvar lv_name ${${${vars[mountpoint]}##/}//\//_}
defvar filesystem xfs
defvar label ${${vars[lv_name]}[1,12]}
defvar root /
[[ $vars[mountpoint] == /* ]] || \
die "$0: mountpoint must be absolute path, got: ${(qqq)vars[mountpoint]}"
require logical_volume %device \?vg_name :size :lv_name
require filesystem :device :label :filesystem \?mkfs_opts
require fstab fstab=${vars[root]%/}/etc/fstab \
:device :mountpoint :filesystem \?opts \?dump \?pass
require mounted :device mountpoint=${vars[root]%/}/${vars[mountpoint]#/}
do_command=( true )
}
confz_syslinux_modules_check() {
checkvars directory
if ! [[ -e $vars[directory]/menu.c32 ]]; then
do_command=( cp -v /usr/share/syslinux/*.c32 $vars[directory]/ )
return 1
else
return 0
fi
}
# install extlinux
confz_extlinux_check() {
checkvars directory
defvar extlinux /sbin/extlinux
defvar install_touch_file $vars[directory]/.extlinux_installed
[[ -e $vars[directory]/extlinux.conf || -e $vars[directory]/syslinux.cfg ]] || \
die "no configuration file for extlinux found"
require syslinux_modules :directory
[[ -e $vars[install_touch_file] ]]
return $?
}
confz_extlinux_do() {
$vars[extlinux] -i -r $vars[directory] || return $?
touch $vars[install_touch_file] || return $?
}
# install GRUB2
confz_grub2_check() {
checkvars device
defvar boot_directory /boot
defvar install_touch_file vars[boot_directory]/.grub2${${vars[device]}//\//.}
[[ -e $vars[install_touch_file] ]]
return $?
}
confz_grub2_do() {
grub2-install --boot-directory=$vars[boot_directory] $vars[device] || return $?
touch $vars[install_touch_file] || return $?
}