#!/bin/bash
VERSION="0.1.6 [11 Nov 2025]"
THIS=$(basename "$0")
COLUMNS="$(stty size 2>/dev/null||echo 80)"; COLUMNS="${COLUMNS##* }"

while getopts ":bhlnvw" optname; do
  case "$optname" in
    "b")	CACHEMODE="writeback";;
    "h")  HELP="y";;
    "l")  CHANGELOG="y";;
    "n")  SKIPTITLE="y";;
    "v")  VIEW="y";;
    "w")  COLUMNS=30000;;
    "?")  echo "Unknown option $OPTARG">&2; exit 1;;
    ":")  echo "No argument value for option $OPTARG">&2; exit 1;;
    *)  # Should not occur
      echo "Unknown error while processing options">&2; exit 1;;
  esac
done
shift $((OPTIND-1))

[[ -z $SKIPTITLE ]] && echo -e "\n$THIS v$VERSION by Dominic (try -h for help)\n${THIS//?/=}\n"

if [[ -n $HELP ]]; then
	HELP="y"
	echo -e "TL;DR: $THIS supercharges a Logical Volume.

$THIS is a bash wrapper script for LVM functions which convert an existing LVM logical volume (LV) into a read/write 'cache LV'. A typical scenario is that an existing LV (in an existing volume group (VG)) is currently stored on one (or more) physical volume(s) (PV(s)) which are on one (or more) slower devices (e.g. spinning disks), and we want to make this LV faster for read and write by (optionally creating and) using a new PV on a smaller but faster device (e.g. NVMe SSD) as a 'cache pool'. For more info see 'man 7 lvmcache'.

After some sanity tests, $THIS asks you (once) to confirm before any action is taken.

When $THIS has completed, the new cache LV will have the same name as, and be usable as, the original LV. There will also be two hidden LVs - the data LV on the original slower drive (named as the original LV with '_corig' suffixed) and a cache pool LV on the faster device (named as the original LV with '_CacheVol_cvol' suffixed). Internally the cache pool LV will hold both the cached data and the metadata for managing the cache. All LVs can be seen with: lvs --all --options +devices

$THIS has been tested under LVM 2.03.11(2) (2021-01-08).

Usage: $THIS [options] existing_VG/existing_LV PV_for_cache [\"further_options_for_lvconvert_(at_cache_creation)\"]

Example:
    # chunksize parameter may be needed with a large PV_for_cache (e.g. 200GB)
    $THIS timedicer/home /dev/sda1 \"--chunksize 512\"

Options: -b use writeback cachemode setting (faster writes, but risk of data loss)
         -h see this help
         -l see changelog
         -n do not show program title
         -v merely show existing cache LVs, if any

Notes:
  The device (typically a drive partition) to be used for the new PV \
(PV_for_cache i.e. the cache pool) must exist, but need not already be a PV. \
It should be much faster than the PV(s) which hold the existing_LV \
(or the conversion is pointless), and \
its contents (if any) must be expendable - they will be lost.

  I have not seen any guidance on the recommended size ratio between the cache pool device (PV_for_cache) and the underlying data LV (existing_LV). Most examples use small cache pools (e.g. 10GB), maybe because they were written when larger SSDs were expensive. I have used a 200GB cache pool (for a 2TB source LV) without problems, except having to specify \"--chunksize 512\" when creating it.

  - Of 2 possible cache methods, $THIS uses 'dm-cache' (alternative 'dm-writecache' only speeds up writing not reading)
  - Of 2 possible cache options, $THIS uses 'cachevol' (the cached copies of data blocks and the metadata for managing the cache are both stored in the same (hidden) LV on the same (fast) device; the more complex alternative option 'cachepool' allows having cache data & metadata on different (LVs and) devices [which could make the cached LV operate marginally faster])
  - Unsurprisingly, to make changes $THIS must be run by superuser
  - I made this utility to help me and you, but you use it at your own risk

  Normally $THIS uses the default and recommended 'writethrough' cachemode setting, but this means that caching gives no speed gain when writing, and may even be slower. Option '-b' (untested at time of writing [22 Dec 2023]) forces use of cachemode 'writeback' - this should bring write speed gains but at the risk of data loss if the cache pool LV (cachevol) is corrupted or lost.

  Note for versions of LVM earlier than 2.03.12 it is not possible to resize logical volumes of cache type, \
including those created by $THIS. To workaround this \
limitation, split and then rejoin the data and cache LVs (but you will \
lose the cache data which will have to be rebuilt over time):
    lvs -a -o +cache_mode,cache_settings,cache_policy,chunk_size # show LVs
    # example: VG 'timedicer', cache LV 'home', non-standard chunksize '512'
    lvconvert --splitcache timedicer/home # split cache LV
    lvs -a -o +cache_mode,cache_settings,cache_policy # show LVs
    lvextend -L+100G -r timedicer/home # extend LV and underlying FS
    # restore cache LV
    lvconvert --type cache --cachevol timedicer/home_CacheVol timedicer/home --chunksize 512
    lvs -a -o +cache_mode,cache_settings,cache_policy,chunk_size # show LVs
With the above lvconvert command it is possible to specify --cachemode writeback (instead of the default --cachemode writethrough) for faster writes, however as of LVM tools 2.03.11(2) this is regarded as unsafe and you will have to ignore/override warnings saying 'repairing a damaged cachevol is not yet possible' and 'cache mode writethrough is suggested for safe operation'. $THIS (except with option '-b') sticks with the default setting.

Another approach which works equally well but may be a little slower (because \
--uncache is slower than --splitcache) involves removing the cache pool LV, \
something like this:
    lvconvert --uncache timedicer/home # may take some time
    lvextend -L+100G -r timedicer/home # extend LV and the underlying FS
    $THIS timedicer/home /dev/sda1 \"--chunksize 512\"

  If you lose the device on which your cache pool is stored, you will have problems, especially because you will also have lost the metadata for the whole LV (so you will not be able to access the underlying LV even though all your data should still be there). Recovery should be possible by using a backup of LVM metadata to recreate the PV, VG and cache pool LV - for guidance try: https://www.golinuxcloud.com/recover-lvm2-partition-restore-vg-pv-metadata/#How_to_manually_delete_LVM_metadata_in_Linux or, in case it has disappeared, then as pdf at: https://www.timedicer.co.uk/programs/helpinfo/Recover_LVM2_partition_PV_VG_LVM_metadata.pdf

Kudos: The following pages were helpful in devising this utility and writing the help text -
https://manpages.ubuntu.com/manpages/noble/en/man7/lvmcache.7.html
https://www.ipcompro.net/IpComPro/Training-Materials/Linux/NoTs/RHEL-8-LVM-ADMIN.pdf (chapter 13)
https://www.stderr.nl/Blog/Software/Linux/LvmResizeCachedLv.html

License: Copyright © 2025 Dominic Raferd. Licensed under the Apache License, \
Version 2.0 (the \"License\"); you may not use this file except in compliance \
with the License. You may obtain a copy of the License at \
https://www.apache.org/licenses/LICENSE-2.0. Unless required by applicable \
law or agreed to in writing, software distributed under the License is \
distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY \
KIND, either express or implied. See the License for the specific language \
governing permissions and limitations under the License.
"|fold -sw"$COLUMNS"
fi
if [[ -n $CHANGELOG ]]; then
	[[ -n $HELP ]] && echo "Changelog:"
	echo -e "\
0.1.6 [11 Nov 2025] - Updated link in kudos section
0.1.5 [23 Sep 2025] - Updated link in kudos section
0.1.4 [14 Jan 2025] - Removed dead link in kudos section, fixed to pass shellcheck
0.1.3 [12 Nov 2024] - Updated link in kudos section
0.1.2 [20 Dec 2023] - Add -b option (enable writeback cachemode) (untested)
0.1.1 [19 Dec 2023] - Help text changes: add info re backup pdf page for data recovery + re splitting cached LV
0.1.0 [14 Dec 2022] - Help text changes, and add hints on what to do if cache pool device goes missing
0.0.6 [16 Nov 2022] - Help text clarifications, bugfix -v option, add -n option
0.0.5 [08 Apr 2022] - Help text clarifications
0.0.4 [06 Apr 2022] - Add help about how to resize/extend a cache type LV, add -v option
0.0.3 [01 Apr 2022] - Help text clarifications
0.0.2 [11 Feb 2022] - Help text clarifications
0.0.1 [02 Feb 2022] - Help text changes, bug fix, check if existing LV is already cached
0.0.0 [11 Jan 2022] - Initial version\n"|fold -sw"$COLUMNS"
fi
[[ -n $HELP$CHANGELOG ]] && exit 0

if [[ -n $VIEW ]]; then
	echo "Listing cache LVs (if any):"
	while read -ra PARAMS; do
		PV=$(pvs --noheadings -o vg_name,lv_name,pv_name --separator=, 2>/dev/null|grep -F " ${PARAMS[0]},[${PARAMS[2]}],"|cut -d, -f3)
		echo "Found cache LV '${PARAMS[0]}/${PARAMS[1]}': its cache pool '${PARAMS[0]}/${PARAMS[2]}' is on '$PV'"
	done < <(lvs --noheadings -o vg_name,lv_name,pool_lv --separator=" "|sed 's/^\s\+//;s/[][]//g'|awk '{if (NF>2) print $0}')
	exit 0
fi

[[ -n $1 && -n $2 ]] || { echo "Invalid parameters, unable to continue" >&2; exit 1; }

NEW_PV="$2"
OLD_LV="$1"
EXISTING_LV="${OLD_LV##*/}"
EXISTING_VG="${OLD_LV%/*}"

# check existing VG & LV
lvs "$EXISTING_VG/$EXISTING_LV" >/dev/null 2>&1 || { echo "Invalid existing VG '$EXISTING_VG' and/or LV '$EXISTING_LV', aborting" >&2; exit 1; }
EXISTING_STATUS=$(lvs --no-headings "$EXISTING_VG/$EXISTING_LV" -o pool_lv,origin|wc -w)
case $EXISTING_STATUS in
	0) echo "Existing LV '$EXISTING_VG/$EXISTING_LV' exists and is not already a cache LV: OK";;
	2) echo "Existing LV '$EXISTING_VG/$EXISTING_LV' is already a cache LV, nothing to do"; exit 1;;
	*) echo "Warning: Unexpected status ($EXISTING_STATUS) found for existing '$EXISTING_VG/$EXISTING_LV', proceed with care!";;
esac

# check new PV exists
ls "$NEW_PV" >/dev/null 2>&1 || { echo "Device to host proposed new PV '$NEW_PV' does not exist, aborting" >&2; exit 1; }
echo "Device to host proposed new PV '$NEW_PV' exists: OK"

echo "PV(s) on this system:"; pvs 2>&1|sed 's/^/  /'
echo "VG(s) on this system:"; vgs 2>&1|sed 's/^/  /'
echo "LV(s) on VG '$EXISTING_VG':"; lvs -a -o +cache_mode "$EXISTING_VG" 2>&1|sed 's/^/  /'

# check that $EXISTING_LV is not already a cache type
EXISTING_TYPE=$(lvs --no-headings -o lv_layout "$EXISTING_VG/$EXISTING_LV"|awk '{print $1}')
if [[ $EXISTING_TYPE == cache ]]; then
	echo "LV '$EXISTING_VG/$EXISTING_LV' is already a cache LV, aborting" >&2; exit 1
elif [[ $EXISTING_TYPE != linear ]]; then
	echo "Warning: LV '$EXISTING_VG/$EXISTING_LV' is of type '$EXISTING_TYPE', not 'linear' as expected!"
fi

[[ $(id -u) -eq 0 ]] || { echo "Can't continue except as superuser, aborting" >&2; exit 1; }

read -r -t20 -p "Convert $EXISTING_VG/$EXISTING_LV into a 'cache LV' using $NEW_PV for caching via dm-cache [y/-]? "
[[ $REPLY == y ]] || { echo "No changes made"; exit 0; }

# create new PV
if pvs "$NEW_PV" >/dev/null 2>&1; then
	echo "PV '$NEW_PV' already exists, no need to create it"
else
	echo "Creating new PV '$NEW_PV'"
	pvcreate "$NEW_PV" || { echo "Problem occurred, aborting" >&2; exit 1; }
	echo "  ... OK"
fi

# extend existing VG onto new PV
NEW_PV_EXISTING_VG=$(pvs --no-headings -o vg_name "$NEW_PV"|sed 's/^\s\+//;s/\s\+$//')
if [[ -z $NEW_PV_EXISTING_VG ]]; then
	echo "Extending '$EXISTING_VG' onto '$NEW_PV'"
	vgextend "$EXISTING_VG" "$NEW_PV" || { echo "Problem occurred, aborting" >&2; exit 1; }
	echo "  ... OK"
elif [[ "$NEW_PV_EXISTING_VG" == "$EXISTING_VG" ]]; then
	echo "PV '$NEW_PV' already belongs to VG '$EXISTING_VG', no need to change"
else
	echo "PV '$NEW_PV' belongs to a different VG '$NEW_PV_EXISTING_VG', aborting" >&2; exit 1
fi

# check we didn't do this already
if lvs -a "$EXISTING_VG/${EXISTING_LV}_CacheVol" >/dev/null 2>&1; then
	echo "'${EXISTING_LV}_CacheVol' already exists as LV on VG '$EXISTING_VG'"
else
	# create LV that will be used for Cache volume (this is briefly a standard LV on the VG but restricted to $NEW_PV)
	echo "Creating new LV '${EXISTING_LV}_CacheVol' using entire '$NEW_PV'"
	lvcreate -n "${EXISTING_LV}_CacheVol" -l 100%PVS "$EXISTING_VG" "$NEW_PV" || { echo "Problem occurred, aborting" >&2; exit 1; }
	echo "  ... OK"
fi
echo "LVs are now:"; lvs -a -o +cache_mode "$EXISTING_VG" 2>&1|sed 's/^/  /'


echo "Combining LVs '$EXISTING_LV' and '${EXISTING_LV}_CacheVol' into new cache LV '$EXISTING_LV'"
if [[ -z $CACHEMODE ]]; then
	# shellcheck disable=SC2086
	lvconvert --type cache --cachevol "${EXISTING_LV}_CacheVol" $3 "$EXISTING_VG/$EXISTING_LV" || { echo "Problem occurred, aborting" >&2; exit 1; }
else
	# shellcheck disable=SC2086
	lvconvert --force --type cache --cachevol "${EXISTING_LV}_CacheVol" --cachemode "$CACHEMODE" $3 "$EXISTING_VG/$EXISTING_LV" || { echo "Problem occurred, aborting" >&2; exit 1; }
fi
echo "  ... OK"

echo -e "All completed apparently ok\nLV '$EXISTING_VG/$EXISTING_LV' was converted to cache LV\nPlease check VG '$EXISTING_VG' LVs:"
lvs -a -o +cache_mode "$EXISTING_VG" 2>&1|sed 's/^/  /'
exit 0
