4 # This script creates ZFS pools and dataset compatible with zsys
8 # bpool/BOOT/ubuntu_${UUID}
9 # rpool/ROOT/ubuntu_${UUID}
10 # rpool/ROOT/ubuntu_${UUID}/var -o canmount=off
11 # rpool/ROOT/ubuntu_${UUID}/var/games
12 # rpool/ROOT/ubuntu_${UUID}/var/lib
13 # rpool/ROOT/ubuntu_${UUID}/var/lib/AccountsService
14 # rpool/ROOT/ubuntu_${UUID}/var/lib/apt
15 # rpool/ROOT/ubuntu_${UUID}/var/lib/dpkg
16 # rpool/ROOT/ubuntu_${UUID}/var/log
17 # rpool/ROOT/ubuntu_${UUID}/var/mail
18 # rpool/ROOT/ubuntu_${UUID}/var/snap
19 # rpool/ROOT/ubuntu_${UUID}/var/spool
20 # rpool/ROOT/ubuntu_${UUID}/var/www
21 # rpool/ROOT/ubuntu_${UUID}/var/lib/NetworkManager
22 # rpool/ROOT/ubuntu_${UUID}/srv
23 # rpool/ROOT/ubuntu_${UUID}/usr -o canmount=off
24 # rpool/ROOT/ubuntu_${UUID}/usr/local
25 # rpool/USERDATA/$user_$UUID2
26 # rpool/USERDATA/root_$UUID2
29 # - Verify that /target is mounted
32 # - delete all the partitions but the ESP
33 # - Create p1 ext4 size 100MB
34 # - Create p2 zfs bpool 1GB
35 # - Create p3 zfs rbool 100% remaining
37 # - Create /swapfile on /target
39 # After setup is done leave it mounted to let Ubiquity proceed with installation
43 REQUIREDPKGS
="zfsutils-linux"
45 ESP
="${TARGET}/boot/efi"
46 ZSYSTMP
="/tmp/$(basename $0)"
47 INIT_FLAG
="${ZSYSTMP}/init.done"
48 FSTAB_PARTMAN
="${ZSYSTMP}/fstab.partman"
49 PARTITION_LAYOUT
="${ZSYSTMP}/layout"
54 # Display script usage
56 Usage: $(basename "$0") [COMMAND] [OPTIONS...]
57 Prepares a zsys compatible ZFS system.
60 layout Get layout to display before formatting to ubiquity. Give the chosen disk as argument
61 init Initialize the pools and datasets
62 finalize Finalize the installation after the system has been installed
65 -d, --debug Enable debug mode
73 TEMP
=$
(getopt
-o $SHORTOPTS --long $LONGOPTS -- "$@")
91 COMMAND
=$
( echo $1|
tr '[:upper:]' '[:lower:]' )
97 check_prerequisites
() {
98 # Check and set requirements to run this script
100 # Check and set the requirements to run this test. If any of the
101 # requirement is missing the programs exit with error
104 # $@: List of required packages
107 # Exit program is a requirement is not met
108 echo "I: Checking system requirements"
110 if [ $
(id
-u) -ne 0 ]; then
111 echo "E: Script must be executed as root. Exiting!"
116 if ! dpkg-query
-W -f'${Status}' "${pkg}"|
grep -q "install ok installed" 2>/dev
/null
; then
117 echo "E: $pkg is required and not installed on this system. Exiting!"
127 if ! grep -qE "\s${target}\s" /proc
/mounts
; then
128 echo "E: $target is not mounted. Exiting!"
132 # Save fstab generated by partman
133 if [ -f "${target}/etc/fstab" ]; then
134 echo "I: Saving existing fstab"
135 cp "${target}/etc/fstab" "${FSTAB_PARTMAN}"
137 echo "W: ${target}/etc/fstab doesn't exist"
141 # It may fail to umount because the swap is being created by partman and not finished when we reach this point.
142 # Give it some time and retry with a sleep between tries.
146 for mountpoint
in "${ESP}" "${target}"; do
147 if [ ! -d "${mountpoint}" ]; then
151 echo "I: umounting ${mountpoint}"
153 # Do not make it quiet. We want to know why it failed.
154 if ! sudo umount
"${mountpoint}"; then
156 echo "W: Try ${iter}. Failed to umount ${mountpoint}."
157 if [ ${iter} -eq ${maxiter} ]; then
158 echo "E: Failed to umount ${mountpoint}. Exiting!"
170 # Returns disk, base name of the partition and partition numbers to create
174 if [ -z "${disk}" ]; then
175 # The entire disk has been formatted with use_device
176 # There is either one ext4 partition or one ext4 and one ESP
177 part
="$(grep -E "\s
${target}\s
" /proc/mounts | awk '{print $1}')"
180 if [ -n "${part}" ]; then
181 disk
="$(lsblk -lns -o TYPE,PATH ${part}| grep disk| awk '{print $2}')"
182 if [ -z "${disk}" ]; then
183 echo "E: Couldn't identify disk for partition ${part}. Exiting!"
186 # Some disks have letters in the partition number like /dev/nvme0n1p1
187 # In this case we want to retrieve 'p' so we deal only with partition number
188 # in the rest of the script and prepend the base.
189 partbase
="$(echo ${part} | sed -e 's/[0-9]*$//' | sed -e "s
#${disk}##")"
192 # The only purpose of this code is to display a friendly message in ubiquity to show the user
193 # what partitioning will be performed. However, on first call, the disk is not yet partitioned
194 # and collecting the information about disk partitioning would require to query partman. But we
195 # don't want to add this extra complexity just to display a message. Instead we hardcode the
196 # extension of the partition name depending on the type of disk, basically it's 'p' for anything
197 # else than standard drives (eg nvme01pX)
199 /dev
/sd
*|
/dev
/hd
*|
/dev
/vd
*)
208 if is_gpt
"${disk}"; then
209 # No extended partition on EFI + GPT
219 # MBR pools are on extended partition
231 echo "OK|${disk}|${partbase}|${partesp}|${partswap}|${partbpool}|${partrpool}"
241 partswap
=$
(( partbpool
- 1 ))
242 partext
=$
(( partesp
+ 1 ))
243 partprefix
="${disk}${partbase}"
245 sfdisktmp
="${ZSYSTMP}/sfdisk.cfg"
248 echo "I: Formatting disk $disk with partitions ESP:${partesp} ext:${partext} swap:${partswap} bpool:${partbpool} rpool:${partrpool}"
250 # bpool size: 500M < 5% of ZFS allocated space < 2G
251 # partswap is partition 2 on GPT systems and first extended partition on MBR.
252 size_percent
=$
(expr \
( $
(blockdev
--getsize64 ${partprefix}${partswap}) / 1024 / 1024 \
) \
* 5 / 100)
254 [ ${size_percent} -gt ${bpool_size} ] && bpool_size=${size_percent}
255 [ ${bpool_size} -gt 2048 ] && bpool_size
=2048
257 if is_gpt
"${disk}"; then
258 # Improvement: Delete all the partitions but the ESP
259 # There should be only 1 or 2 partitions but it can be made generic
260 if ! esp_exists
"${disk}"; then
261 start
=$
(sfdisk
-l "${disk}"|grep "^${partprefix}${partesp}"|awk '{print $2}')
262 cat > "${sfdisktmp}" <<EOF
263 ${partprefix}${partesp} : start= ${start}, size= 512M, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B, bootable
266 sfdisk --delete "${disk}" ${partswap}
269 cat >> "${sfdisktmp}" <<EOF
270 ${partprefix}${partswap} : size= ${ss}M, type=0657FD6D-A4AB-43C4-84E5-0933C84B4F4F
271 ${partprefix}${partbpool} : size= ${bpool_size}M, type=6A82CB45-1DD2-11B2-99A6-080020736631
272 ${partprefix}${partrpool} : type=6A85CF4D-1DD2-11B2-99A6-080020736631
275 if ! esp_exists "${disk}"; then
276 start=$(sfdisk -l "${disk}"|grep "^${partprefix}${partesp}"|awk '{print $2}')
277 cat > "${sfdisktmp}" <<EOF
278 ${partprefix}${partesp} : start= ${start}, size= 512M, type=ef, bootable
281 sfdisk --delete "${disk}" ${partswap}
284 cat >> "${sfdisktmp}" <<EOF
285 ${partprefix}${partswap} : size= ${ss}M, type=82
286 ${partprefix}${partbpool} : size= ${bpool_size}M, type=a5
287 ${partprefix}${partrpool} : type=a5
291 cat "${sfdisktmp}" | sfdisk --append "${disk}"
293 # Force a re-read of the partition table
294 echo "I: Re-reading partition table"
295 partx --add "${disk}" 2>/dev/null || true
296 partx --show "${disk}"
304 echo "I: Initializing ZFS"
305 # Now we can create the pools and dataset
306 UUID_ORIG=$(head -100 /dev/urandom | tr -dc 'a-z0-9
' |head -c6)
313 -O acltype=posixacl \
316 -O normalization=formD \
321 -O mountpoint=/ -R "${target}" rpool "${partrpool}"
324 # The version of bpool is set to the default version to prevent users from upgrading
325 # Then only features supported by grub are enabled.
329 -o feature@async_destroy=enabled \
330 -o feature@bookmarks=enabled \
331 -o feature@embedded_data=enabled \
332 -o feature@empty_bpobj=enabled \
333 -o feature@enabled_txg=enabled \
334 -o feature@extensible_dataset=enabled \
335 -o feature@filesystem_limits=enabled \
336 -o feature@hole_birth=enabled \
337 -o feature@large_blocks=enabled \
338 -o feature@lz4_compress=enabled \
339 -o feature@spacemap_histogram=enabled \
341 -O acltype=posixacl \
344 -O normalization=formD \
347 -O mountpoint=/boot -R "${target}" bpool "${partbpool}"
349 # Root and boot dataset
350 zfs create rpool/ROOT -o canmount=off -o mountpoint=none
351 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}" -o mountpoint=/
352 zfs create bpool/BOOT -o canmount=off -o mountpoint=none
353 zfs create "bpool/BOOT/ubuntu_${UUID_ORIG}" -o mountpoint=/boot
356 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var" -o canmount=off
357 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/lib"
358 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/lib/AccountsService"
359 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/lib/apt"
360 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/lib/dpkg"
361 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/lib/NetworkManager"
363 # Desktop specific system dataset
364 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/srv"
365 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/usr" -o canmount=off
366 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/usr/local"
367 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/games"
368 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/log"
369 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/mail"
370 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/snap"
371 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/spool"
372 zfs create "rpool/ROOT/ubuntu_${UUID_ORIG}/var/www"
375 # Dataset associated to the user are created by the installer.
376 zfs create rpool/USERDATA -o canmount=off -o mountpoint=/
378 # Set zsys properties
379 zfs set com.ubuntu.zsys:bootfs='yes' "rpool/ROOT/ubuntu_${UUID_ORIG}"
380 zfs set com.ubuntu.zsys:last-used=$(date +%s) "rpool/ROOT/ubuntu_${UUID_ORIG}"
381 zfs set com.ubuntu.zsys:bootfs='no
' "rpool/ROOT/ubuntu_${UUID_ORIG}/srv"
382 zfs set com.ubuntu.zsys:bootfs='no
' "rpool/ROOT/ubuntu_${UUID_ORIG}/usr"
383 zfs set com.ubuntu.zsys:bootfs='no
' "rpool/ROOT/ubuntu_${UUID_ORIG}/var"
392 echo "I: Creating user $user with home $userhome"
393 mv "${target}/${userhome}" "${target}/tmp/home/${user}"
394 zfs create "rpool/USERDATA/${user}_${uuid}" -o canmount=on -o mountpoint=${userhome}
395 chown $(chroot "${target}" id -u ${user}):$(chroot ${target} id -g ${user}) "${target}/${userhome}"
396 rsync -a "${target}/tmp/home/${user}/" "${target}/${userhome}"
397 bootfsdataset=$(grep "\s${target}\s" /proc/mounts | awk '{ print
$1 }')
398 zfs set com.ubuntu.zsys:bootfs-datasets="${bootfsdataset}" rpool/USERDATA/${user}_${UUID_ORIG}
401 init_system_partitions() {
407 mkdir -p "${target}/boot/efi"
408 mount -t vfat "${partefi}" "${target}/boot/efi"
409 mkdir -p "${target}/boot/efi/grub"
411 echo "I: Mount grub directory"
412 # Finalize grub directory
413 mkdir -p "${target}/boot/grub"
414 mount -o bind "${target}/boot/efi/grub" "${target}/boot/grub"
418 if is_gpt "${1}"; then
419 parttype="C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
421 # FIXME: Currently partman-auto set the type of EFI on MBR as W95 (b) instead of EFI (ef)
424 sfdisk -d "${1}" | grep -q "type=${parttype}"
428 sfdisk -d "${1}" | awk '/^label
:/ {print
$2}'|grep -q gpt
431 check_prerequisites ${REQUIREDPKGS}
433 echo "I: Running $(basename "$0") ${COMMAND}"
435 if [ -z "${COMMAND}" ]; then
436 echo "E: ${COMMAND} is mandatory. Exiting!"
438 elif [ "${COMMAND}" = "layout" ]; then
439 # Just displays de layout that will be created without any change to the disk.
440 # At this stage we don't now yet the size of the partition that will be created.
441 IFS
="|" read ERR DISK PARTBASE PARTESP PARTSWAP PARTBPOOL PARTRPOOL
<<EOF
442 $(get_layout ${TARGET} "${EXTRAARG}")
445 if [ "${ERR}" != "OK" ]; then
450 cat > "${PARTITION_LAYOUT}" <<EOF
453 if ! esp_exists
"${DISK}"; then
454 cat >> "${PARTITION_LAYOUT}" <<EOF
455 part:vfat:ESP:${DISK}${PARTBASE}${PARTESP}
459 cat >> "${PARTITION_LAYOUT}" <<EOF
460 part:swap:swap:${DISK}${PARTBASE}${PARTSWAP}
461 part:zfs:bpool:${DISK}${PARTBASE}${PARTBPOOL}
462 part:zfs:rpool:${DISK}${PARTBASE}${PARTRPOOL}
465 elif [ "${COMMAND}" = "init" ]; then
468 IFS
="|" read ERR DISK PARTBASE PARTESP PARTSWAP PARTBPOOL PARTRPOOL
<<EOF
469 $(get_layout ${TARGET} "")
472 if [ "${ERR}" != "OK" ]; then
477 echo "I: Partition table before init of ZFS"
478 partx
--show "${DISK}"
480 # Swap files are not supported on ZFS, we use a swap partition instead:
481 SWAPFILE
="$(grep "^
${TARGET}" /proc/swaps |awk '{print $1}')"
482 # Give us a minimum swap partition size of 4MB in case we decide on
483 # no swap, just to keep the partition layout stable:
486 # Disable swap and get the swap volume size:
487 if [ -n "${SWAPFILE}" ]; then
488 SWAPSIZE
=$
(stat
-c%s
"${SWAPFILE}")
489 echo "I: Found swapfile with size ${SWAPSIZE}. Disabling"
490 swapoff
"${SWAPFILE}"
492 # Convert to MiB to align the size on the size of a block
493 SWAPVOLSIZE
=$
(( SWAPSIZE
/ 1024 / 1024 ))
495 prepare_target
"${TARGET}"
496 format_disk
"${DISK}" "${PARTBASE}" "${PARTESP}" "${PARTBPOOL}" "${PARTRPOOL}" "${SWAPVOLSIZE}"
497 init_zfs
"${TARGET}" "${DISK}${PARTBASE}${PARTBPOOL}" "${DISK}${PARTBASE}${PARTRPOOL}"
498 init_system_partitions
"${TARGET}" "${DISK}${PARTBASE}1" "${DISK}${PARTBASE}${PARTESP}"
501 # $TARGET/etc has been destroyed by the creation of the zfs partitition
503 mkdir
-p "${TARGET}/etc"
504 if [ -f "${FSTAB_PARTMAN}" ]; then
505 echo "I: Creating fstab"
506 grep -Ev '\s/\s|/swapfile' "${FSTAB_PARTMAN}" > "${TARGET}/etc/fstab"
509 if ! grep -q "boot/efi" "${TARGET}/etc/fstab"; then
510 espuuid
=$
(blkid
-s UUID
-o value
"${DISK}${PARTBASE}${PARTESP}")
511 echo "UUID=${espuuid}\t/boot/efi\tvfat\tumask=0022,fmask=0022,dmask=0022\t0\t1" >> "${TARGET}/etc/fstab"
514 # Bind mount grub from ESP to the expected location
515 echo "/boot/efi/grub\t/boot/grub\tnone\tdefaults,bind\t0\t0" >> "${TARGET}/etc/fstab"
517 if [ -n "${SWAPFILE}" ]; then
518 SWAPDEVICE
="${DISK}${PARTBASE}${PARTSWAP}"
519 mkswap
-f "${SWAPDEVICE}"
520 SWAPID
=$
(blkid
-s UUID
-o value
"${SWAPDEVICE}")
521 printf "UUID=${SWAPID}\tnone\tswap\tdiscard\t0\t0\n" >> "${TARGET}/etc/fstab"
522 swapon
-v "${SWAPDEVICE}"
524 # Make /boot/{grub,efi} world readable
525 sed -i 's#\(.*boot/efi.*\)umask=0077\(.*\)#\1umask=0022,fmask=0022,dmask=0022\2#' "${TARGET}/etc/fstab"
527 echo "I: Marking ZFS utilities to be kept in the target system"
528 apt-install zfsutils-linux
2>/dev
/null
529 apt-install zfs-initramfs
2>/dev
/null
530 apt-install zsys
2>/dev
/null
533 elif [ "${COMMAND}" = "finalize" ]; then
534 if [ ! -f "$INIT_FLAG" ]; then
535 echo "W: zsys init didn't succeed. Not proceeding with command: ${COMMAND}. Aborting!"
539 # Activate zfs generator.
540 # After enabling the generator we should run zfs set canmount=on DATASET
541 # in the chroot for one dataset of each pool to refresh the zfs cache.
542 echo "I: Activating zfs generator"
543 ln -s /usr
/lib
/zfs-linux
/zed.d
/history_event-zfs-list-cacher.sh
"${TARGET}/etc/zfs/zed.d"
546 zpool
set cachefile
= bpool
547 zpool
set cachefile
= rpool
548 cp /etc
/zfs
/zpool.cache
"${TARGET}/etc/zfs/"
549 mkdir
-p "${TARGET}/etc/zfs/zfs-list.cache"
550 touch "${TARGET}/etc/zfs/zfs-list.cache/bpool" "${TARGET}/etc/zfs/zfs-list.cache/rpool"
553 UUID_ORIG
=$
(head -100 /dev
/urandom |
tr -dc 'a-z0-9' |
head -c6)
554 mkdir
-p "${TARGET}/tmp/home"
555 for user
in ${TARGET}/home
/*; do
556 if [ -d "${user}" ]; then
557 user
="$(basename $user)"
558 move_user
"${TARGET}" "${user}" "/home/${user}" "${UUID_ORIG}"
562 move_user
"${TARGET}" root
/root
"${UUID_ORIG}"
564 echo "I: Changing sync mode of rpool to standard"
565 zfs
set sync
=standard rpool
567 echo "I: ZFS setup complete"
569 echo "E: Unknown command: $COMMAND"