diff --git a/README.md b/README.md index 2cb6f4b..50f7b3b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ A reliable snapshot/rollback capability is a key feature required to enable the These are the roles included in the collection. Follow the links below to see the detailed documentation and example playbooks for each role. - [`lvm_snapshots`](./roles/lvm_snapshots/) - controls creation and rollback for a defined set of LVM snapshot volumes +- [`bigboot`](./roles/bigboot/) - controls increasing of the boot partition while moving, and shrinking if needed, the adjacent partition +- [`initramfs`](./roles/initramfs/) - controls the atomic flow of building and using a temporary initramfs in a reboot and restoring the original one +- [`shrink_lv`](./roles/shrink_lv/) - controls decreasing logical volume size along with the filesystem Additional roles are planned to support shrinking logical volumes to make free space available in a volume group and relocating physical volumes to enable increasing the size of a /boot /partition. diff --git a/changelogs/fragments/add-bigboot-role.yml b/changelogs/fragments/add-bigboot-role.yml new file mode 100644 index 0000000..e0c164d --- /dev/null +++ b/changelogs/fragments/add-bigboot-role.yml @@ -0,0 +1,3 @@ +major_changes: + - New role, initramfs, to execute an atomic flow of building and using a temporary initramfs in a reboot and restoring the original one + - New role, bigboot, to increase the boot partition while moving, and shrinking if needed, the adjacent partition diff --git a/changelogs/fragments/add-shrink-lv.yml b/changelogs/fragments/add-shrink-lv.yml new file mode 100644 index 0000000..d1b2839 --- /dev/null +++ b/changelogs/fragments/add-shrink-lv.yml @@ -0,0 +1,2 @@ +major_changes: + - New role, shrink_lv, to decrease logical volume size along with the filesystem diff --git a/roles/bigboot/README.md b/roles/bigboot/README.md new file mode 100644 index 0000000..a901d7a --- /dev/null +++ b/roles/bigboot/README.md @@ -0,0 +1,42 @@ +# bigboot + +The `bigboot` role is used to increase boot partition. + +The role is designed to support the automation of RHEL in-place upgrades, but can also be used for other purposes. + +## Contents + +The role contains the shell scripts to increase the size of the boot partition, as well as the script wrapping it to run as part of the pre-mount step during the boot process. +Finally, there is a copy of the [`sfdisk`](https://man7.org/linux/man-pages/man8/sfdisk.8.html) binary with version `2.38.1` to ensure the extend script will work regardless of the `util-linux` package installed in the target host. + +## Role Variables + +### `bigboot_size` + +The variable `bigboot_size` sets the target size of the boot partition after the role has completed. +The value can be either in bytes or with optional single letter suffix (1024 bases). +See `Unit options` type `iec` of [`numfmt`](https://man7.org/linux/man-pages/man1/numfmt.1.html) + + +## Example of a playbook to run the role +The following yaml is an example of a playbook that runs the role against a group of hosts named `rhel` and increasing the size of its boot partition by 1G. +The boot partition is automatically retrieved by the role by identifying the existing mounted partition to `/boot` and passing the information to the script using the `kernel_opts`. + +```yaml +- name: Extend boot partition playbook + hosts: all + vars: + bigboot_size: 1G + roles: + - bigboot +``` + +# Validate execution +The script will add an entry to the kernel messages (`/dev/kmsg`) with success or failure and the time it took to process. +In case of failure, it may also include an error message retrieved from the execution of the script. + +A successful execution will look similar to this: +```bash +[root@localhost ~]# dmesg |grep pre-mount +[ 357.163522] [dracut-pre-mount] Boot partition /dev/vda1 successfully increased by 1G (356 seconds) +``` diff --git a/roles/bigboot/defaults/main.yaml b/roles/bigboot/defaults/main.yaml new file mode 100644 index 0000000..bbd3f74 --- /dev/null +++ b/roles/bigboot/defaults/main.yaml @@ -0,0 +1 @@ +bigboot_size: diff --git a/roles/bigboot/files/bigboot.sh b/roles/bigboot/files/bigboot.sh new file mode 100644 index 0000000..4aa6a99 --- /dev/null +++ b/roles/bigboot/files/bigboot.sh @@ -0,0 +1,540 @@ +#!/bin/bash + +# Command parameters +INCREMENT_BOOT_PARTITION_SIZE= +DEVICE_NAME= +BOOT_PARTITION_NUMBER= +PARTITION_PREFIX= + +# Script parameters +ADJACENT_PARTITION_NUMBER= +BOOT_FS_TYPE= +EXTENDED_PARTITION_TYPE=extended +LOGICAL_VOLUME_DEVICE_NAME= +INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES= +SHRINK_SIZE_IN_BYTES= + +print_help(){ + echo "" + echo "Script to increase the ext4/xfs boot partition in a BIOS system by shifting the adjacent partition to the boot partition by the parametrized size." + echo "It expects the device to have enough free space to shift to the right of the adjacent partition, that is towards the end of the device." + echo "It only works with ext4 and xfs file systems and supports adjacent partitions as primary or logical partitions and LVM in the partition." + echo "" + echo "The script determines which partition number is the boot partition by looking for the boot flag." + echo "This process won't work with an EFI boot partition because the boot partition that contains the kernel and the initramfs are in a partition that does not have the flag." + echo "The parametrized size supports M for MiB and G for GiB. If no units is given, it is interpreted as bytes" + echo "" + echo "Usage: $(basename "$0") " + echo "" + echo "Example" + echo " Given this device partition:" + echo " Number Start End Size Type File system Flags" + echo " 32.3kB 1049kB 1016kB Free Space" + echo " 1 1049kB 11.1GB 11.1GB primary ext4 boot" + echo " 2 11.1GB 32.2GB 21.1GB extended" + echo " 5 11.1GB 32.2GB 21.1GB logical ext4" + echo "" + echo " Running the command:" + echo " $>$(basename "$0") /dev/vdb 1G" + echo " or" + echo " $>$(basename "$0") /dev/vdb 1073741824" + echo "" + echo " Will increase the boot partition in /dev/vdb by 1G and shift the adjacent partition in the device by the equal amount." + echo "" + echo " Number Start End Size Type File system Flags" + echo " 32.3kB 1049kB 1016kB Free Space" + echo " 1 1049kB 12.2GB 12.2GB primary ext4 boot" + echo " 2 12.2GB 32.2GB 20.0GB extended" + echo " 5 12.2GB 32.2GB 20.0GB logical ext4" +} + +get_device_type(){ + local device=$1 + val=$(/usr/bin/lsblk "$device" -o type --noheadings 2>&1) + local status=$? + if [[ status -ne 0 ]]; then + echo "Failed to retrieve device type for $device: $val" + exit 1 + fi + type=$(tail -n1 <<<"$val") + if [[ -z $type ]]; then + echo "Unknown device type for $device" + exit 1 + fi + echo "$type" +} + +ensure_device_not_mounted() { + local device=$1 + local devices_to_check + device_type=$(get_device_type "$device") + if [[ $device_type == "lvm" ]]; then + # It's an LVM block device + # Capture the LV device names. Since we'll have to shift the partition, we need to make sure all LVs are not mounted in the adjacent partition. + devices_to_check=$(/usr/sbin/lvm pvdisplay "$device" -m |/usr/bin/grep "Logical volume" |/usr/bin/awk '{print $3}') + else + # Use the device and partition number instead + devices_to_check=$device + fi + for device_name in $devices_to_check; do + /usr/bin/findmnt --source "$device_name" 1>&2>/dev/null + status=$? + if [[ status -eq 0 ]]; then + echo "Device $device_name is mounted" + exit 1 + fi + done +} + +validate_device() { + local device=$1 + if [[ -z "${device}" ]]; then + echo "Missing device name" + print_help + exit 1 + fi + if [[ ! -e "${device}" ]]; then + echo "Device ${device} not found" + print_help + exit 1 + fi + ret=$(/usr/sbin/fdisk -l "${device}" 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to open device ${device}: $ret" + exit 1 + fi +} + +validate_increment_partition_size() { + if [[ -z "$INCREMENT_BOOT_PARTITION_SIZE" ]]; then + echo "Missing incremental size for boot partition" + print_help + exit 1 + fi + ret=$(/usr/bin/numfmt --from=iec "$INCREMENT_BOOT_PARTITION_SIZE" 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Invalid size value for '$INCREMENT_BOOT_PARTITION_SIZE': $ret" + exit $status + fi + INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES=$ret +} + +# Capture all parameters: +# Mandatory: Device, Size and Boot Partition Number +# Optional: Parition Prefix (e.g. "p" for nvme based volumes) +parse_flags() { + for i in "$@" + do + case $i in + -d=*|--device=*) + DEVICE_NAME=(${i#*=}) + ;; + -s=*|--size=*) + INCREMENT_BOOT_PARTITION_SIZE=(${i#*=}) + ;; + -b=*|--boot=*) + BOOT_PARTITION_NUMBER=(${i#*=}) + ;; + -p=*|--prefix=*) + PARTITION_PREFIX=(${i#*=}) + ;; + -h) + print_help + exit 0 + ;; + *) + # unknown option + echo "Unknown flag $i" + print_help + exit 1 + ;; + esac + done +} + +validate_parameters() { + validate_device "${DEVICE_NAME}" + validate_increment_partition_size + + # Make sure BOOT_PARTITION_NUMBER is set to avoid passing only DEVICE_NAME + if [[ -z "$BOOT_PARTITION_NUMBER" ]]; then + echo "Boot partition number was not set" + print_help + exit 1 + fi + validate_device "${DEVICE_NAME}${PARTITION_PREFIX}${BOOT_PARTITION_NUMBER}" + + ensure_device_not_mounted "${DEVICE_NAME}${PARTITION_PREFIX}${BOOT_PARTITION_NUMBER}" + ensure_extendable_fs_type "${DEVICE_NAME}${PARTITION_PREFIX}${BOOT_PARTITION_NUMBER}" +} + +get_fs_type(){ + local device=$1 + ret=$(/usr/sbin/blkid "$device" -o udev | sed -n -e 's/ID_FS_TYPE=//p' 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + exit $status + fi + echo "$ret" +} + +ensure_extendable_fs_type(){ + local device=$1 + ret=$(get_fs_type "$device") + if [[ "$ret" != "ext4" ]] && [[ "$ret" != "xfs" ]]; then + echo "Boot file system type $ret is not extendable" + exit 1 + fi + BOOT_FS_TYPE=$ret +} + +get_successive_partition_number() { + boot_line_number=$(/usr/sbin/parted -m "$DEVICE_NAME" print |/usr/bin/sed -n '/^'"$BOOT_PARTITION_NUMBER"':/ {=}') + status=$? + if [[ $status -ne 0 ]]; then + echo "Unable to identify boot partition number for '$DEVICE_NAME'" + exit $status + fi + if [[ -z "$boot_line_number" ]]; then + echo "No boot partition found" + exit 1 + fi + # get the extended partition number in case there is one, we will need to shrink it as well + EXTENDED_PARTITION_NUMBER=$(/usr/sbin/parted "$DEVICE_NAME" print | /usr/bin/sed -n '/'"$EXTENDED_PARTITION_TYPE"'/p'|awk '{print $1}') + if [[ -n "$EXTENDED_PARTITION_NUMBER" ]]; then + # if there's an extended partition, use the last one as the target partition to shrink + ADJACENT_PARTITION_NUMBER=$(/usr/sbin/parted "$DEVICE_NAME" print |grep -v "^$" |awk 'END{print$1}') + else + # get the partition number from the next line after the boot partition + ADJACENT_PARTITION_NUMBER=$(/usr/sbin/parted -m "$DEVICE_NAME" print | /usr/bin/awk -F ':' '/'"^$BOOT_PARTITION_NUMBER:"'/{getline;print $1}') + fi + if ! [[ $ADJACENT_PARTITION_NUMBER == +([[:digit:]]) ]]; then + echo "Invalid successive partition number '$ADJACENT_PARTITION_NUMBER'" + exit 1 + fi + ensure_device_not_mounted "${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" +} + +init_variables(){ + parse_flags "$@" + validate_parameters + get_successive_partition_number +} + +check_filesystem(){ + local device=$1 + fstype=$(get_fs_type "$device") + if [[ "$fstype" == "swap" ]]; then + echo "Warning: cannot run fsck to a swap partition for $device" + return 0 + fi + if [[ "$BOOT_FS_TYPE" == "ext4" ]]; then + # Retrieve the estimated minimum size in bytes that the device can be shrank + ret=$(/usr/sbin/e2fsck -fy "$device" 2>&1) + local status=$? + if [[ status -ne 0 ]]; then + echo "Warning: File system check failed for $device: $ret" + fi + fi +} + +convert_size_to_fs_blocks(){ + local device=$1 + local size=$2 + block_size_in_bytes=$(/usr/sbin/tune2fs -l "$device" | /usr/bin/awk '/Block size:/{print $3}') + echo $(( size / block_size_in_bytes )) +} + +calculate_expected_resized_file_system_size_in_blocks(){ + local device=$1 + increment_boot_partition_in_blocks=$(convert_size_to_fs_blocks "$device" "$INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES") + total_block_count=$(/usr/sbin/tune2fs -l "$device" | /usr/bin/awk '/Block count:/{print $3}') + new_fs_size_in_blocks=$(( total_block_count - increment_boot_partition_in_blocks )) + echo $new_fs_size_in_blocks +} + +get_free_device_size() { + free_space=$(/usr/sbin/parted -m "$DEVICE_NAME" unit b print free | /usr/bin/awk -F':' '/'"^$ADJACENT_PARTITION_NUMBER:"'/{getline;print $0}'|awk -F':' '/free/{print $4}'|sed -e 's/B//g') + echo "$free_space" +} + +get_volume_group_name(){ + local volume_group_name + ret=$(/usr/sbin/lvm pvs "${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" -o vg_name --noheadings|/usr/bin/sed 's/^[[:space:]]*//g') + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to retrieve volume group name for logical volume $LOGICAL_VOLUME_DEVICE_NAME: $ret" + exit $status + fi + echo "$ret" +} + +deactivate_volume_group(){ + local volume_group_name + volume_group_name=$(get_volume_group_name) + ret=$(/usr/sbin/lvm vgchange -an "$volume_group_name" 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to deactivate volume group $volume_group_name: $ret" + exit $status + fi + # avoid potential deadlocks with udev rules before continuing + sleep 1 +} + +check_available_free_space(){ + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + free_device_space_in_bytes=$(get_free_device_size) + # if there is enough free space after the adjacent partition, there is no need to shrink it. + if [[ $free_device_space_in_bytes -gt $INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES ]]; then + SHRINK_SIZE_IN_BYTES=0 + return + fi + SHRINK_SIZE_IN_BYTES=$((INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES-free_device_space_in_bytes)) + device_type=$(get_device_type "${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}") + if [[ "$device_type" == "lvm" ]]; then + # there is not enough free space after the adjacent partition, calculate how much extra space is needed + # to be fred from the PV + local volume_group_name + volume_group_name=$(get_volume_group_name) + pe_size_in_bytes=$(/usr/sbin/lvm pvdisplay "$device" --units b| /usr/bin/awk 'index($0,"PE Size") {print $3}') + unusable_space_in_pv_in_bytes=$(/usr/sbin/lvm pvdisplay --units B "$device" | /usr/bin/awk 'index($0,"not usable") {print $(NF-1)}'|/usr/bin/numfmt --from=iec) + total_pe_count_in_vg=$(/usr/sbin/lvm vgs "$volume_group_name" -o pv_pe_count --noheadings) + allocated_pe_count_in_vg=$(vgs "$volume_group_name" -o pv_pe_alloc_count --noheadings) + free_pe_count=$((total_pe_count_in_vg - allocated_pe_count_in_vg)) + # factor in the unusable space to match the required number of free PEs + required_pe_count=$(((SHRINK_SIZE_IN_BYTES+unusable_space_in_pv_in_bytes)/pe_size_in_bytes)) + if [[ $required_pe_count -gt $free_pe_count ]]; then + echo "Not enough available free PE in VG $volume_group_name: Required $required_pe_count but found $free_pe_count" + exit 1 + fi + fi +} + +resolve_device_name(){ + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + device_type=$(get_device_type "$device") + if [[ $device_type == "lvm" ]]; then + # It's an LVM block device + # Determine which is the last LV in the PV + # shellcheck disable=SC2016 + device=$(/usr/sbin/lvm pvdisplay "$device" -m | /usr/bin/sed -n '/Logical volume/h; ${x;p;}' | /usr/bin/awk '{print $3}') + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to identify the last LV in $device" + exit $status + fi + # Capture the LV device name + LOGICAL_VOLUME_DEVICE_NAME=$device + fi +} + +check_device(){ + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + resolve_device_name + ensure_device_not_mounted "$device" + check_available_free_space +} + +evict_end_PV() { + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + local shrinking_start_PE=$1 + ret=$(/usr/sbin/lvm pvmove --alloc anywhere "$device":"$shrinking_start_PE"- 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to move PEs in PV $LOGICAL_VOLUME_DEVICE_NAME: $ret" + exit $status + fi + check_filesystem "$LOGICAL_VOLUME_DEVICE_NAME" +} + +shrink_physical_volume() { + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + pe_size_in_bytes=$(/usr/sbin/lvm pvdisplay "$device" --units b| /usr/bin/awk 'index($0,"PE Size") {print $3}') + unusable_space_in_pv_in_bytes=$(/usr/sbin/lvm pvdisplay --units B "$device" | /usr/bin/awk 'index($0,"not usable") {print $(NF-1)}'|/usr/bin/numfmt --from=iec) + + total_pe_count=$(/usr/sbin/lvm pvs "$device" -o pv_pe_count --noheadings | /usr/bin/sed 's/^[[:space:]]*//g') + evict_size_in_PE=$((SHRINK_SIZE_IN_BYTES/pe_size_in_bytes)) + shrink_start_PE=$((total_pe_count - evict_size_in_PE)) + pv_new_size_in_bytes=$(( (shrink_start_PE*pe_size_in_bytes) + unusable_space_in_pv_in_bytes )) + + ret=$(/usr/sbin/lvm pvresize --setphysicalvolumesize "$pv_new_size_in_bytes"B -t "$device" -y 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + if [[ $status -eq 5 ]]; then + # ERRNO 5 is equivalent to command failed: https://github.com/lvmteam/lvm2/blob/2eb34edeba8ffc9e22b6533e9cb20e0b5e93606b/tools/errors.h#L23 + # Try to recover by evicting the ending PEs elsewhere in the PV, in case it's a failure due to ending PE's being inside the shrinking area. + evict_end_PV $shrink_start_PE + else + echo "Failed to resize PV $device: $ret" + exit $status + fi + fi + echo "Shrinking PV $device to $pv_new_size_in_bytes bytes" >&2 + ret=$(/usr/sbin/lvm pvresize --setphysicalvolumesize "$pv_new_size_in_bytes"B "$device" -y 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to resize PV $device during retry: $ret" + exit $status + fi + check_filesystem "$LOGICAL_VOLUME_DEVICE_NAME" +} + +calculate_new_end_partition_size_in_bytes(){ + local partition_number=$1 + local device="${DEVICE_NAME}${PARTITION_PREFIX}${partition_number}" + current_partition_size_in_bytes=$(/usr/sbin/parted -m "$DEVICE_NAME" unit b print| /usr/bin/awk '/^'"$partition_number"':/ {split($0,value,":"); print value[3]}'| /usr/bin/sed -e's/B//g') + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to convert new device size to megabytes $device: $ret" + exit 1 + fi + + new_partition_size_in_bytes=$(( current_partition_size_in_bytes - SHRINK_SIZE_IN_BYTES)) + echo "$new_partition_size_in_bytes" +} + +shrink_partition() { + local partition_number=$1 + new_end_partition_size_in_bytes=$(calculate_new_end_partition_size_in_bytes "$partition_number") + echo "Shrinking partition $partition_number in $DEVICE_NAME" >&2 + ret=$(echo Yes | /usr/sbin/parted "$DEVICE_NAME" ---pretend-input-tty unit B resizepart "$partition_number" "$new_end_partition_size_in_bytes" 2>&1 ) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to resize device $DEVICE_NAME$partition_number to size: $ret" + exit 1 + fi +} + +shrink_adjacent_partition(){ + if [[ $SHRINK_SIZE_IN_BYTES -eq 0 ]]; then + # no need to shrink the PV or the partition as there is already enough free available space after the partition holding the PV + return 0 + fi + local device_type + device_type=$(get_device_type "${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}") + if [[ "$device_type" == "lvm" ]]; then + shrink_physical_volume + fi + shrink_partition "$ADJACENT_PARTITION_NUMBER" + if [[ -n "$EXTENDED_PARTITION_NUMBER" ]]; then + # resize the extended partition + shrink_partition "$EXTENDED_PARTITION_NUMBER" + fi +} + +shift_adjacent_partition() { + # If boot partition is not the last one, shift the successive partition to the right to take advantage of the newly fred space. Use 'echo ',' | sfdisk --move-data -N + # to shift the partition to the right. + # The astute eye will notice that we're moving the partition, not the last logical volume in the partition. + local target_partition=$ADJACENT_PARTITION_NUMBER + if [[ -n "$EXTENDED_PARTITION_NUMBER" ]]; then + target_partition=$EXTENDED_PARTITION_NUMBER + fi + echo "Moving up partition $target_partition in $DEVICE_NAME by $INCREMENT_BOOT_PARTITION_SIZE" >&2 + ret=$(echo "+$INCREMENT_BOOT_PARTITION_SIZE,"| /usr/sbin/sfdisk --move-data "$DEVICE_NAME" -N "$target_partition" --force 2>&1) + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to shift partition '$DEVICE_NAME$target_partition': $ret" + exit $status + fi +} + +update_kernel_partition_tables(){ + # Ensure no size inconsistencies between PV and partition + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + device_type=$(get_device_type "$device") + if [[ $device_type == "lvm" ]]; then + ret=$(/usr/sbin/lvm pvresize "$device" -y 2>&1) + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to align PV and partition sizes '$device': $ret" + exit $status + fi + # ensure that the VG is not active so that the changes to the kernel PT are reflected by the partprobe command + deactivate_volume_group + fi + /usr/sbin/partprobe "$DEVICE_NAME" 2>&1 + if [[ $device_type == "lvm" ]]; then + # reactivate volume group + activate_volume_group + fi +} + +increase_boot_partition() { + local device="${DEVICE_NAME}${PARTITION_PREFIX}${BOOT_PARTITION_NUMBER}" + local new_fs_size_in_blocks= + echo "Increasing boot partition $BOOT_PARTITION_NUMBER in $DEVICE_NAME by $INCREMENT_BOOT_PARTITION_SIZE" >&2 + ret=$(echo "- +"| /usr/sbin/sfdisk "$DEVICE_NAME" -N "$BOOT_PARTITION_NUMBER" --no-reread --force 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to shift boot partition '$device': $ret" + return + fi + update_kernel_partition_tables + # Extend the boot file system with `resize2fs ` + if [[ "$BOOT_FS_TYPE" == "ext4" ]]; then + check_filesystem "$device" + increment_boot_partition_in_blocks=$(convert_size_to_fs_blocks "$device" "$INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES") + total_block_count=$(/usr/sbin/tune2fs -l "$device" | /usr/bin/awk '/Block count:/{print $3}') + new_fs_size_in_blocks=$(( total_block_count + increment_boot_partition_in_blocks )) + ret=$(/usr/sbin/resize2fs "$device" $new_fs_size_in_blocks 2>&1) + elif [[ "$BOOT_FS_TYPE" == "xfs" ]]; then + block_size_in_bytes=$(/usr/sbin/xfs_db "$device" -c "sb" -c "print blocksize" |/usr/bin/awk '{print $3}') + current_blocks_in_data=$(/usr/sbin/xfs_db "$device" -c "sb" -c "print dblocks" |/usr/bin/awk '{print $3}') + increment_boot_partition_in_blocks=$((INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES/block_size_in_bytes)) + new_fs_size_in_blocks=$((current_blocks_in_data + increment_boot_partition_in_blocks)) + # xfs_growfs requires the file system to be mounted in order to change its size + # Create a temporal directory + tmp_dir=$(/usr/bin/mktemp -d) + # Mount the boot file system in the temporal directory + /usr/bin/mount "$device" "$tmp_dir" + ret=$(/usr/sbin/xfs_growfs "$device" -D "$new_fs_size_in_blocks" 2>&1) + # Capture the status + status=$? + # Unmount the file system + /usr/bin/umount "$device" + else + echo "Device $device does not contain an ext4 or xfs file system: $BOOT_FS_TYPE" + return + fi + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to resize boot partition '$device': $ret" + return + fi + echo "Boot file system increased to $new_fs_size_in_blocks blocks" >&2 +} + +activate_volume_group(){ + local device="${DEVICE_NAME}${PARTITION_PREFIX}${ADJACENT_PARTITION_NUMBER}" + local volume_group_name + volume_group_name=$(get_volume_group_name) + ret=$(/usr/sbin/lvm vgchange -ay "$volume_group_name" 2>&1) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to activate volume group $volume_group_name: $ret" + exit $status + fi + # avoid potential deadlocks with udev rules before continuing + sleep 1 +} + +# last steps are to run the fsck on boot partition and activate the volume gruop if necessary +cleanup(){ + # run a file system check to the boot file system + check_filesystem "${DEVICE_NAME}${PARTITION_PREFIX}${BOOT_PARTITION_NUMBER}" +} + +main() { + init_variables "$@" + check_device + shrink_adjacent_partition + shift_adjacent_partition + increase_boot_partition + cleanup +} + +main "$@" diff --git a/roles/bigboot/files/module-setup.sh b/roles/bigboot/files/module-setup.sh new file mode 100644 index 0000000..1c1ecac --- /dev/null +++ b/roles/bigboot/files/module-setup.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh + +check(){ + return 0 +} + +install() { + inst_multiple -o /usr/bin/mount /usr/bin/umount /usr/sbin/parted /usr/bin/mktemp /usr/bin/wc /usr/bin/date /usr/bin/sed /usr/bin/awk /usr/bin/basename /usr/sbin/resize2fs /usr/sbin/tune2fs /usr/sbin/partprobe /usr/bin/numfmt /usr/sbin/lvm /usr/bin/lsblk /usr/sbin/e2fsck /usr/sbin/fdisk /usr/bin/findmnt /usr/bin/tail /usr/ /usr/sbin/xfs_growfs /usr/sbin/xfs_db + inst_hook pre-mount 99 "$moddir/increase-boot-partition.sh" + inst_binary "$moddir/sfdisk.static" "/usr/sbin/sfdisk" + inst_simple "$moddir/bigboot.sh" "/usr/bin/bigboot.sh" +} diff --git a/roles/bigboot/files/sfdisk.static b/roles/bigboot/files/sfdisk.static new file mode 100755 index 0000000..5f5dc6d Binary files /dev/null and b/roles/bigboot/files/sfdisk.static differ diff --git a/roles/bigboot/tasks/get_boot_device_info.yml b/roles/bigboot/tasks/get_boot_device_info.yml new file mode 100644 index 0000000..7b63205 --- /dev/null +++ b/roles/bigboot/tasks/get_boot_device_info.yml @@ -0,0 +1,20 @@ +- name: Find the boot mount entry + ansible.builtin.set_fact: + _boot_mount_entry: "{{ ansible_facts.mounts | selectattr('mount', 'equalto', '/boot') | first }}" + +- name: Calculate the partition to look for + ansible.builtin.set_fact: + _boot_partition_name: "{{ (_boot_mount_entry.device | split('/'))[-1] }}" + +- name: Find the boot device parent + ansible.builtin.set_fact: + _boot_disk: "{{ item.key }}" + with_dict: "{{ ansible_facts.devices }}" + when: _boot_partition_name in item.value.partitions + +- name: Capture boot device details + ansible.builtin.set_fact: + boot_device_partition_prefix: "{{ _boot_partition_name[(_boot_disk | length) : -1] }}" + boot_partition_number: "{{ _boot_partition_name[-1] }}" + boot_device_name: "/dev/{{ _boot_disk }}" + boot_device_original_size: "{{ _boot_mount_entry.size_total | int }}" diff --git a/roles/bigboot/tasks/main.yaml b/roles/bigboot/tasks/main.yaml new file mode 100644 index 0000000..3b819a8 --- /dev/null +++ b/roles/bigboot/tasks/main.yaml @@ -0,0 +1,70 @@ +--- +- name: Make sure the required related facts are available + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - mounts + - devices + +- name: Validate bigboot_size is not empty + ansible.builtin.assert: + that: bigboot_size | length >0 + fail_msg: "bigboot_size is empty" + +- name: Validate initramfs preflight + ansible.builtin.include_role: + name: initramfs + tasks_from: preflight + +- name: Get boot device info + ansible.builtin.include_tasks: + file: get_boot_device_info.yml + +- name: Copy extend boot dracut module + ansible.builtin.copy: + src: "{{ item }}" + dest: /usr/lib/dracut/modules.d/99extend_boot/ + mode: "0554" + loop: + - bigboot.sh + - module-setup.sh + - sfdisk.static + +- name: Resolve and copy the shrink-start script + ansible.builtin.template: + src: increase-boot-partition.sh.j2 + dest: /usr/lib/dracut/modules.d/99extend_boot/increase-boot-partition.sh + mode: '0554' + +- name: Create the initramfs and reboot to run the module + vars: + initramfs_add_modules: "extend_boot" + ansible.builtin.include_role: + name: initramfs + +- name: Remove dracut extend boot module + ansible.builtin.file: + path: /usr/lib/dracut/modules.d/99extend_boot + state: absent + +- name: Retrieve mount points + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - mounts + +- name: Capture boot device new size + ansible.builtin.set_fact: + boot_device_new_size: "{{ (ansible_facts.mounts | selectattr('mount', 'equalto', '/boot') | first).size_total | int }}" + +- name: Capture expected device size + ansible.builtin.set_fact: + expected_size: "{{ (bigboot_size | human_to_bytes | int) + (boot_device_original_size | int) }}" + +- name: Validate boot partition new size + ansible.builtin.assert: + that: + - boot_device_new_size != boot_device_original_size + fail_msg: "Boot partition size '{{ boot_device_new_size }}' did not change" diff --git a/roles/bigboot/templates/increase-boot-partition.sh.j2 b/roles/bigboot/templates/increase-boot-partition.sh.j2 new file mode 100644 index 0000000..35075fd --- /dev/null +++ b/roles/bigboot/templates/increase-boot-partition.sh.j2 @@ -0,0 +1,31 @@ +#!/bin/bash + +disable_lvm_lock(){ + tmpfile=$(/usr/bin/mktemp) + sed -e 's/\(^[[:space:]]*\)locking_type[[:space:]]*=[[:space:]]*[[:digit:]]/\1locking_type = 1/' /etc/lvm/lvm.conf >"$tmpfile" + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to disable lvm lock: $status" >/dev/kmsg + exit 1 + fi + # replace lvm.conf. There is no need to keep a backup since it's an ephemeral file, we are not replacing the original in the initramfs image file + mv "$tmpfile" /etc/lvm/lvm.conf +} + +main() { + name=$(basename "$0") + start=$(/usr/bin/date +%s) + disable_lvm_lock + # run bigboot.sh to increase boot partition and file system size + ret=$(sh /usr/bin/bigboot.sh -d="{{ boot_device_name }}" -s="{{ bigboot_size }}" -b="{{ boot_partition_number }}" -p="{{ boot_device_partition_prefix }}" 2>/dev/kmsg) + status=$? + end=$(/usr/bin/date +%s) + # write the log file + if [[ $status -eq 0 ]]; then + echo "[$name] Boot partition {{ boot_device_name }} successfully increased by {{ bigboot_size }} ("$((end-start))" seconds) " >/dev/kmsg + else + echo "[$name] Failed to extend boot partition: $ret ("$((end-start))" seconds)" >/dev/kmsg + fi +} + +main "$0" diff --git a/roles/initramfs/README.md b/roles/initramfs/README.md new file mode 100644 index 0000000..01d41a6 --- /dev/null +++ b/roles/initramfs/README.md @@ -0,0 +1,59 @@ +# initramfs + +The `initramfs` role is used to run an atomic flow of building and using a temporary initramfs in a reboot and restoring the original one. + +The role is designed to be internal for this collection and support the automation of RHEL in-place upgrades, but can also be used for other purposes. + +## Contents + +To allow fast fail, the role provides a [`preflight.yml`](./tasks/preflight.yml) tasks file to be used at the start of the playbook. +Please note that the [`main`](./tasks/main.yml) task file will not run the preflight checks + +## Role Variables + +All variables are optional + +### `initramfs_add_modules` + +`initramfs_add_modules` is a a space-separated list of dracut modules to be added to the default set of modules. +See [`dracut`](https://man7.org/linux/man-pages/man8/dracut.8.html) `-a` parameter for details. + +### `initramfs_backup_extension` + +`initramfs_backup_extension` is the file extension for the backup initramfs file. + +Defaults to `old` + +### `initramfs_post_reboot_delay` + +`initramfs_post_reboot_delay` sets the amount of Seconds to wait after the reboot command was successful before attempting to validate the system rebooted successfully. +The value is used for [`post_reboot_delay`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/reboot_module.html#parameter-post_reboot_delay) parameter + +Defaults to `30` + +### `initramfs_reboot_timeout` + +`initramfs_reboot_timeout` sets the maximum seconds to wait for machine to reboot and respond to a test command. +The value is used for [`reboot_timeout`](https://docs.ansible.com/ansible/latest/collections/ansible/builtin/reboot_module.html#parameter-reboot_timeout) parameter + +Defaults to `7200` + + +## Example of a playbook to run the role +The following yaml is an example of a playbook that runs the role against a group of hosts named `rhel` and increasing the size of its boot partition by 1G. +The boot partition is automatically retrieved by the role by identifying the existing mounted partition to `/boot` and passing the information to the script using the `kernel_opts`. + +```yaml +- name: Extend boot partition playbook + hosts: all + tasks: + - name: Validate initramfs preflight + ansible.builtin.include_role: + name: initramfs + tasks_from: preflight + - name: Create the initramfs and reboot to run the module + vars: + initramfs_add_modules: "my_extra_module" + ansible.builtin.include_role: + name: initramfs +``` diff --git a/roles/initramfs/defaults/main.yml b/roles/initramfs/defaults/main.yml new file mode 100644 index 0000000..06d94e7 --- /dev/null +++ b/roles/initramfs/defaults/main.yml @@ -0,0 +1,4 @@ +initramfs_backup_extension: old +initramfs_add_modules: "" +initramfs_post_reboot_delay: 30 +initramfs_reboot_timeout: 7200 diff --git a/roles/initramfs/tasks/main.yml b/roles/initramfs/tasks/main.yml new file mode 100644 index 0000000..e3acc93 --- /dev/null +++ b/roles/initramfs/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Make sure the required related facts are available + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - kernel + +- name: Get kernel version + ansible.builtin.set_fact: + _kernel_version: "{{ ansible_facts.kernel }}" + +- name: Create a backup of the current initramfs + ansible.builtin.copy: + remote_src: true + src: /boot/initramfs-{{ _kernel_version }}.img + dest: /boot/initramfs-{{ _kernel_version }}.img.{{ initramfs_backup_extension }} + mode: "0600" + +- name: Create a new initramfs with the optional additional modules + # yamllint disable-line rule:line-length + ansible.builtin.command: '/usr/sbin/dracut {{ ((initramfs_add_modules | length) > 0) | ternary("-a", "") }} "{{ initramfs_add_modules }}" --kver {{ _kernel_version }} --force' + changed_when: true + +- name: Reboot the server + ansible.builtin.reboot: + post_reboot_delay: "{{ initramfs_post_reboot_delay }}" + reboot_timeout: "{{ initramfs_reboot_timeout }}" + +- name: Restore previous initramfs + ansible.builtin.copy: + remote_src: true + src: /boot/initramfs-{{ _kernel_version }}.img.{{ initramfs_backup_extension }} + dest: /boot/initramfs-{{ _kernel_version }}.img + mode: "0600" + +- name: Remove initramfs backup file + ansible.builtin.file: + path: /boot/initramfs-{{ _kernel_version }}.img.{{ initramfs_backup_extension }} + state: absent diff --git a/roles/initramfs/tasks/preflight.yml b/roles/initramfs/tasks/preflight.yml new file mode 100644 index 0000000..ced1fa1 --- /dev/null +++ b/roles/initramfs/tasks/preflight.yml @@ -0,0 +1,27 @@ +--- +- name: Make sure the required related facts are available + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - kernel + +- name: Get kernel version + ansible.builtin.set_fact: + _kernel_version: "{{ ansible_facts.kernel }}" + +- name: Get default kernel + ansible.builtin.command: + cmd: /sbin/grubby --default-kernel + register: _grubby_rc + changed_when: false + +- name: Parse default kernel version + ansible.builtin.set_fact: + _default_kernel: "{{ ((((_grubby_rc.stdout_lines[0] | split('/'))[2] | split('-'))[1:]) | join('-')) | trim }}" + +- name: Check the values + ansible.builtin.assert: + that: _default_kernel == _kernel_version + fail_msg: "Current kernel version '{{ _kernel_version }}' is not the default version '{{ _default_kernel }}'" + success_msg: "Current kernel version {{ _kernel_version }} and default version {{ _default_kernel }} match" diff --git a/roles/shrink_lv/README.md b/roles/shrink_lv/README.md new file mode 100644 index 0000000..a74597d --- /dev/null +++ b/roles/shrink_lv/README.md @@ -0,0 +1,55 @@ +# shrink_lv + +The `shrink_lv` role is used to decrease the size of logical volumes and the file system within them. + +The role is designed to support the automation of RHEL in-place upgrades, but can also be used for other purposes. + +## Contents + +The role contains the shell scripts to shrink the logical volume and file system, as well as the script wrapping it to run as part of the pre-mount step during the boot process. + +## Role Variables + +### `shrink_lv_devices` + +The variable `shrink_lv_devices` is the list of logical volumes to shrink and the target size for those volumes. + +#### `device` + +The device that is mounted as listed under `/proc/mount`. +If the same device has multiple paths, e.g. `/dev/vg/lv` and `/dev/mapper/vg/lv` pass the path that is mounted + +#### `size` + +The target size of the logical volume and filesystem after the role has completed. +The value can be either in bytes or with optional single letter suffix (1024 bases). +See `Unit options` type `iec` of [`numfmt`](https://man7.org/linux/man-pages/man1/numfmt.1.html) + +## Example of a playbook to run the role + +The following yaml is an example of a playbook that runs the role against all hosts to shrink the logical volume `lv` in volume group `vg` to 4G. + +```yaml +- name: Shrink Logical Volumes playbook + hosts: all + vars: + shrink_lv_devices: + - device: /dev/vg/lv + size: 4G + roles: + - shrink_lv +``` + +# Validate execution +The script will add an entry to the kernel messages (`/dev/kmsg` or `/var/log/messages`) with success or failure. +In case of failure, it may also include an error message retrieved from the execution of the script. + +A successful execution will look similar to this: +```bash +[root@localhost ~]# cat /var/log/messages |grep Resizing -A 2 -B 2 +Oct 16 17:55:00 localhost /dev/mapper/rhel-root: 29715/2686976 files (0.2% non-contiguous), 534773/10743808 blocks +Oct 16 17:55:00 localhost dracut-pre-mount: resize2fs 1.42.9 (28-Dec-2013) +Oct 16 17:55:00 localhost journal: Resizing the filesystem on /dev/mapper/rhel-root to 9699328 (4k) blocks.#012The filesystem on /dev/mapper/rhel-root is now 9699328 blocks long. +Oct 16 17:55:00 localhost journal: Size of logical volume rhel/root changed from 40.98 GiB (10492 extents) to 37.00 GiB (9472 extents). +Oct 16 17:55:00 localhost journal: Logical volume rhel/root successfully resized. +``` diff --git a/roles/shrink_lv/defaults/main.yaml b/roles/shrink_lv/defaults/main.yaml new file mode 100644 index 0000000..8d530b7 --- /dev/null +++ b/roles/shrink_lv/defaults/main.yaml @@ -0,0 +1 @@ +shrink_lv_backup_extension: old diff --git a/roles/shrink_lv/files/module-setup.sh b/roles/shrink_lv/files/module-setup.sh new file mode 100644 index 0000000..125f78c --- /dev/null +++ b/roles/shrink_lv/files/module-setup.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh + +check(){ + return 0 +} + +install() { + inst_multiple -o /usr/bin/numfmt /usr/bin/findmnt /usr/bin/lsblk /usr/sbin/lvm /usr/bin/awk /usr/bin/sed /usr/bin/mktemp /usr/bin/date /usr/bin/head /usr/sbin/blockdev /usr/sbin/tune2fs /usr/sbin/resize2fs /usr/bin/cut /usr/sbin/fsadm /usr/sbin/fsck.ext4 + inst_hook pre-mount 99 "$moddir/shrink-start.sh" + inst_simple "$moddir/shrink.sh" "/usr/bin/shrink.sh" +} diff --git a/roles/shrink_lv/files/shrink.sh b/roles/shrink_lv/files/shrink.sh new file mode 100755 index 0000000..125d746 --- /dev/null +++ b/roles/shrink_lv/files/shrink.sh @@ -0,0 +1,235 @@ +#!/bin/bash +VOLUME_SIZE_ALIGNMENT=4096 + +function get_device_name() { + if [[ "$1" == "UUID="* ]]; then + dev_name=$( parse_uuid "$1" ) + else + dev_name=$(/usr/bin/cut -d " " -f 1 <<< "$1") + fi + status=$? + if [[ status -ne 0 ]]; then + return $status + fi + echo "$dev_name" + return $status +} + +function ensure_size_in_bytes() { + local expected_size=$(/usr/bin/numfmt --from iec "$1") + let expected_size=(${expected_size} + $VOLUME_SIZE_ALIGNMENT)/$VOLUME_SIZE_ALIGNMENT*$VOLUME_SIZE_ALIGNMENT + echo $expected_size +} + +function is_device_mounted() { + /usr/bin/findmnt --source "$1" 1>&2>/dev/null + status=$? + if [[ status -eq 0 ]]; then + echo "Device $1 is mounted" >&2 + return 1 + fi + return 0 +} + +function get_current_volume_size() { + val=$(/usr/bin/lsblk -b "$1" -o SIZE --noheadings) + status=$? + if [[ $status -ne 0 ]]; then + return $status + fi + echo "$val" + return 0 +} + +function is_lvm(){ + val=$( /usr/bin/lsblk "$1" --noheadings -o TYPE 2>&1) + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to list block device properties for $2: $val" >&2 + return 1 + fi + if [[ "$val" != "lvm" ]]; then + echo "Device $device_name is not of lvm type" >&2 + return 1 + fi + return 0 +} + +function parse_uuid() { + uuid=$(/usr/bin/awk '{print $1}'<<< "$1"|/usr/bin/awk -F'UUID=' '{print $2}') + val=$(/usr/bin/lsblk /dev/disk/by-uuid/"$uuid" -o NAME --noheadings 2>/dev/null) + status=$? + if [[ $status -ne 0 ]]; then + echo "Failed to retrieve device name for UUID=$uuid" >&2 + return $status + fi + echo "/dev/mapper/$val" + return 0 +} + +function shrink_volume() { + /usr/sbin/lvm lvreduce --resizefs -L "$2b" "$1" + return $? +} + +function check_volume_size() { + current_size=$(get_current_volume_size "$1") + if [[ $current_size -lt $2 ]];then + echo "Current volume size for device $1 ($current_size bytes) is lower to expected $2 bytes" >&2 + return 1 + fi + if [[ $current_size -eq $2 ]]; then + echo "Current volume size for device $1 already equals $2 bytes" >&2 + return 1 + fi + return $? +} + +function convert_size_to_fs_blocks(){ + local device=$1 + local size=$2 + block_size_in_bytes=$(/usr/sbin/tune2fs -l "$device" | /usr/bin/awk '/Block size:/{print $3}') + echo $(( size / block_size_in_bytes )) +} + +function calculate_expected_resized_file_system_size_in_blocks(){ + local device=$1 + increment_boot_partition_in_blocks=$(convert_size_to_fs_blocks "$device" "$INCREMENT_BOOT_PARTITION_SIZE_IN_BYTES") + total_block_count=$(/usr/sbin/tune2fs -l "$device" | /usr/bin/awk '/Block count:/{print $3}') + new_fs_size_in_blocks=$(( total_block_count - increment_boot_partition_in_blocks )) + echo $new_fs_size_in_blocks +} + +function check_filesystem_size() { + local device=$1 + local new_fs_size_in_blocks=$2 + new_fs_size_in_blocks=$(calculate_expected_resized_file_system_size_in_blocks "$device") +# it is possible that running this command after resizing it might give an even smaller number. + minimum_blocks_required=$(/usr/sbin/resize2fs -P "$device" 2> /dev/null | /usr/bin/awk '{print $NF}') + + if [[ "$new_fs_size_in_blocks" -le "0" ]]; then + echo "Unable to shrink volume: New size is 0 blocks" + return 1 + fi + if [[ $minimum_blocks_required -gt $new_fs_size_in_blocks ]]; then + echo "Unable to shrink volume: Estimated minimum size of the file system $1 ($minimum_blocks_required blocks) is greater than the new size $new_fs_size_in_blocks blocks" >&2 + return 1 + fi + return 0 +} + +function process_entry() { + is_lvm "$1" "$3" + status=$? + if [[ $status -ne 0 ]]; then + return "$status" + fi + expected_size_in_bytes=$(ensure_size_in_bytes "$2") + check_filesystem_size "$1" "$expected_size_in_bytes" + status=$? + if [[ $status -ne 0 ]]; then + return "$status" + fi + check_volume_size "$1" "$expected_size_in_bytes" + status=$? + if [[ $status -ne 0 ]]; then + return "$status" + fi + is_device_mounted "$1" + status=$? + if [[ $status -ne 0 ]]; then + return "$status" + fi + shrink_volume "$1" "$expected_size_in_bytes" + return $? +} + +function display_help() { + echo "Program to shrink ext4 file systems hosted in Logical Volumes. + + Usage: '$(basename "$0")' [-h] [-d=|--device=] + + Example: + + where: + -h show this help text + -d|--device= name or UUID of the device that holds an ext4 and the new size separated by a ':' + for example /dev/my_group/my_vol:2G + Sizes will be rounded to be 4K size aligned" +} + +function parse_flags() { + for i in "$@" + do + case $i in + -d=*|--device=*) + entries+=(${i#*=}) + ;; + -h) + display_help + exit 0 + ;; + *) + # unknown option + echo "Unknown flag $i" + display_help + exit 1 + ;; + esac + done + if [[ ${#entries[@]} == 0 ]]; then + display_help + exit 0 + fi +} + +function parse_entry() { + IFS=':' + read -a strarr <<< "$1" + + if [[ ${#strarr[@]} != 2 ]]; then + echo "Invalid device entry $1" + display_help + return 1 + fi + + device="${strarr[0]}" + expected_size="${strarr[1]}" +} + +function main() { + + local -a entries=() + local run_status=0 + + parse_flags "$@" + + for entry in "${entries[@]}" + do + local device + local expected_size + parse_entry "$entry" + status=$? + if [[ $status -ne 0 ]]; then + run_status=$status + continue + fi + device_name=$( get_device_name "$device" ) + status=$? + if [[ $status -ne 0 ]]; then + run_status=$status + continue + fi + + process_entry "$device_name" "$expected_size" "$device" + + status=$? + if [[ $status -ne 0 ]]; then + run_status=$status + fi + done + + exit $run_status +} + +main "$@" diff --git a/roles/shrink_lv/tasks/check_device.yaml b/roles/shrink_lv/tasks/check_device.yaml new file mode 100644 index 0000000..e240c4a --- /dev/null +++ b/roles/shrink_lv/tasks/check_device.yaml @@ -0,0 +1,20 @@ +- name: Get the mount point info + ansible.builtin.set_fact: + _mount_info: "{{ ansible_facts.mounts | selectattr('device', 'equalto', item.device) }}" + +- name: Assert that the mount point exists + ansible.builtin.assert: + that: (_mount_info | length) == 1 + fail_msg: "Mount point {{ item.device }} does not exist" + +- name: Assert that the filesystem is supported + ansible.builtin.assert: + that: _mount_info[0].fstype in ['ext4'] + fail_msg: "Unsupported filesystem '{{ _mount_info[0].fstype }}' on '{{ item.device }}'" + +- name: Assert that the filesystem has enough free space + ansible.builtin.assert: + that: _mount_info[0].block_size * _mount_info[0].block_used < (item.size | ansible.builtin.human_to_bytes) + fail_msg: > + Requested size {{ item.size }} is smaller than currently used + {{ (_mount_info[0].block_size * _mount_info[0].block_used) | ansible.builtin.human_readable }} diff --git a/roles/shrink_lv/tasks/main.yaml b/roles/shrink_lv/tasks/main.yaml new file mode 100644 index 0000000..5f48229 --- /dev/null +++ b/roles/shrink_lv/tasks/main.yaml @@ -0,0 +1,37 @@ +--- +- name: Make sure the required facts are available + ansible.builtin.setup: + gather_subset: + - "!all" + - "!min" + - kernel + - mounts + +- name: Run preflight checks + ansible.builtin.include_tasks: preflight.yaml + +- name: Copy shrink LV dracut module + ansible.builtin.copy: + src: "{{ item }}" + dest: /usr/lib/dracut/modules.d/99shrink_lv/ + mode: "0554" + loop: + - module-setup.sh + - shrink.sh + +- name: Resolve and copy the shrink-start script + ansible.builtin.template: + src: shrink-start.sh.j2 + dest: /usr/lib/dracut/modules.d/99shrink_lv/shrink-start.sh + mode: '0554' + +- name: Create the initramfs and reboot to run the module + vars: + initramfs_add_modules: "shrink_lv" + ansible.builtin.include_role: + name: initramfs + +- name: Remove dracut extend boot module + ansible.builtin.file: + path: /usr/lib/dracut/modules.d/99shrink_lv + state: absent diff --git a/roles/shrink_lv/tasks/preflight.yaml b/roles/shrink_lv/tasks/preflight.yaml new file mode 100644 index 0000000..f5dd0dc --- /dev/null +++ b/roles/shrink_lv/tasks/preflight.yaml @@ -0,0 +1,17 @@ +--- +- name: Assert shrink_lv_devices + ansible.builtin.assert: + that: + - shrink_lv_devices is defined + - shrink_lv_devices | type_debug == "list" + - shrink_lv_devices | length > 0 + fail_msg: shrink_lv_devices must be a list and include at least one element + +- name: Validate initramfs preflight + ansible.builtin.include_role: + name: initramfs + tasks_from: preflight + +- name: Check all devices + ansible.builtin.include_tasks: check_device.yaml + loop: "{{ shrink_lv_devices }}" diff --git a/roles/shrink_lv/templates/shrink-start.sh.j2 b/roles/shrink_lv/templates/shrink-start.sh.j2 new file mode 100644 index 0000000..5d0278b --- /dev/null +++ b/roles/shrink_lv/templates/shrink-start.sh.j2 @@ -0,0 +1,27 @@ +#!/bin/bash + +disable_lvm_lock(){ + tmpfile=$(/usr/bin/mktemp) + sed -e 's/\(^[[:space:]]*\)locking_type[[:space:]]*=[[:space:]]*[[:digit:]]/\1locking_type = 1/' /etc/lvm/lvm.conf >"$tmpfile" + status=$? + if [[ status -ne 0 ]]; then + echo "Failed to disable lvm lock: $status" >/dev/kmsg + exit 1 + fi + # replace lvm.conf. There is no need to keep a backup since it's an ephemeral file, we are not replacing the original in the initramfs image file + mv "$tmpfile" /etc/lvm/lvm.conf +} + +activate_volume_groups(){ + for vg in `/usr/sbin/lvm vgs -o name --noheading 2>/dev/null`; do + /usr/sbin/lvm vgchange -ay $vg + done +} + +main() { + activate_volume_groups + disable_lvm_lock + /usr/bin/shrink.sh {% for device in shrink_lv_devices %}--device={{ device.device }}:{{ device.size }} {% endfor %} 1>&2 >/dev/kmsg +} + +main "$0"