Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib/deploy: Use fallocate for early prune space check #2866

Merged
merged 5 commits into from
May 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 54 additions & 18 deletions src/libostree/ostree-sysroot-deploy.c
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,30 @@ get_kernel_layout_size (OstreeSysroot *self, OstreeDeployment *deployment, guint
return TRUE;
}

/* This is a roundabout but more trustworthy way of doing a space check than
* relying on statvfs's f_bfree when you know the size of the objects. */
static gboolean
dfd_fallocate_check (int dfd, __off_t len, gboolean *out_passed, GError **error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what would be more efficient and cleaner here is to allocate a temporary fd per file we're going to copy, and then actually use the them to do the writes.

But that's also a lot more code changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. I mentioned passing down the fd in the commit message, but yeah... churn.

{
g_auto (GLnxTmpfile) tmpf = {
0,
};
if (!glnx_open_tmpfile_linkable_at (dfd, ".", O_WRONLY | O_CLOEXEC, &tmpf, error))
return FALSE;

*out_passed = TRUE;
/* There's glnx_try_fallocate, but not with the same error semantics. */
if (TEMP_FAILURE_RETRY (fallocate (tmpf.fd, 0, 0, len)) < 0)
{
if (G_IN_SET (errno, ENOSYS, EOPNOTSUPP))
return TRUE;
else if (errno != ENOSPC)
return glnx_throw_errno_prefix (error, "fallocate");
*out_passed = FALSE;
}
return TRUE;
}

/* Analyze /boot and figure out if the new deployments won't fit in the
* remaining space. If they won't, check if deleting the deployments that are
* getting rotated out (e.g. the current rollback) would free up sufficient
Expand Down Expand Up @@ -2534,7 +2558,7 @@ auto_early_prune_old_deployments (OstreeSysroot *self, GPtrArray *new_deployment
continue;
}

guint64 bootdir_size;
guint64 bootdir_size = 0;
if (!get_kernel_layout_size (self, deployment, &bootdir_size, cancellable, error))
return FALSE;

Expand All @@ -2553,32 +2577,45 @@ auto_early_prune_old_deployments (OstreeSysroot *self, GPtrArray *new_deployment
net_new_bootcsum_dirs_total_size += bootdir_size;
}

/* get bootfs free space */
struct statvfs stvfsbuf;
if (TEMP_FAILURE_RETRY (fstatvfs (self->boot_fd, &stvfsbuf)) < 0)
return glnx_throw_errno_prefix (error, "fstatvfs(boot)");

guint64 available_size = stvfsbuf.f_bsize * stvfsbuf.f_bfree;

/* does the bootfs have enough free space for net-new bootdirs? */
if (net_new_bootcsum_dirs_total_size <= available_size)
return TRUE; /* nothing to do! */
{
gboolean bootfs_has_space = FALSE;
if (!dfd_fallocate_check (self->boot_fd, net_new_bootcsum_dirs_total_size, &bootfs_has_space,
error))
return glnx_prefix_error (error, "Checking if bootfs has space");

/* does the bootfs have enough free space for temporarily holding both the new
* and old bootdirs? */
if (bootfs_has_space)
return TRUE; /* nothing to do! */
}

/* OK, we would fail if we tried to write the new bootdirs. Is it salvageable?
* First, calculate how much space we could save with the bootcsums scheduled
* for removal. */
guint64 size_to_remove = 0;
guint64 bootcsum_dirs_to_remove_total_size = 0;
GLNX_HASH_TABLE_FOREACH_KV (current_bootcsums, const char *, bootcsum, gpointer, sizep)
{
if (!g_hash_table_contains (new_bootcsums, bootcsum))
size_to_remove += GPOINTER_TO_UINT (sizep);
bootcsum_dirs_to_remove_total_size += GPOINTER_TO_UINT (sizep);
}

if (net_new_bootcsum_dirs_total_size > (available_size + size_to_remove))
if (net_new_bootcsum_dirs_total_size > bootcsum_dirs_to_remove_total_size)
{
/* Even if we auto-pruned, the new bootdirs wouldn't fit. Just let the
* code continue and let it hit ENOSPC. */
return TRUE;
/* Check whether if we did early prune, we'd have enough space to write
* the new bootcsum dirs. */
gboolean bootfs_has_space = FALSE;
if (!dfd_fallocate_check (
self->boot_fd, net_new_bootcsum_dirs_total_size - bootcsum_dirs_to_remove_total_size,
&bootfs_has_space, error))
return glnx_prefix_error (error, "Checking if bootfs has space");

if (!bootfs_has_space)
{
/* Even if we auto-pruned, the new bootdirs wouldn't fit. Just let the
* code continue and let it hit ENOSPC. */
g_printerr ("Disabling auto-prune optimization; insufficient space left in bootfs\n");
return TRUE;
}
}

g_printerr ("Insufficient space left in bootfs; updating bootloader in two steps\n");
Expand Down Expand Up @@ -2910,7 +2947,6 @@ lint_deployment_fs (OstreeSysroot *self, OstreeDeployment *deployment, int deplo
g_auto (GLnxDirFdIterator) dfd_iter = {
0,
};
glnx_autofd int dest_dfd = -1;
gboolean exists;

if (!ot_dfd_iter_init_allow_noent (deployment_dfd, "var", &dfd_iter, &exists, error))
Expand Down
49 changes: 45 additions & 4 deletions tests/kolainst/destructive/auto-prune.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ set -xeuo pipefail
cd /root
mkdir -p rootfs/usr/lib/modules/`uname -r`
cp /usr/lib/modules/`uname -r`/vmlinuz rootfs/usr/lib/modules/`uname -r`
echo 1 >> rootfs/usr/lib/modules/`uname -r`/vmlinuz
dd if=/dev/urandom of=rootfs/usr/lib/modules/`uname -r`/vmlinuz count=1 conv=notrunc status=none
ostree commit --base "${host_refspec}" -P --tree=dir=rootfs -b modkernel1
echo 1 >> rootfs/usr/lib/modules/`uname -r`/vmlinuz
dd if=/dev/urandom of=rootfs/usr/lib/modules/`uname -r`/vmlinuz count=1 conv=notrunc status=none
ostree commit --base "${host_refspec}" -P --tree=dir=rootfs -b modkernel2

assert_bootfs_has_n_bootcsum_dirs() {
Expand All @@ -25,8 +25,9 @@ assert_bootfs_has_n_bootcsum_dirs() {
}

consume_bootfs_space() {
local free_blocks=$(stat --file-system /boot -c '%a')
local block_size=$(stat --file-system /boot -c '%s')
local free_blocks block_size
free_blocks=${1:-$(stat --file-system /boot -c '%a')}
block_size=$(stat --file-system /boot -c '%s')
# leave 1 block free
unshare -m bash -c \
"mount -o rw,remount /boot && \
Expand All @@ -46,6 +47,7 @@ rpm-ostree rebase :modkernel1
if OSTREE_SYSROOT_OPTS=early-prune ostree admin finalize-staged |& tee out.txt; then
assert_not_reached "successfully wrote to filled up bootfs"
fi
assert_file_has_content out.txt "Disabling auto-prune optimization; insufficient space left in bootfs"
assert_file_has_content out.txt "No space left on device"
rm out.txt
unconsume_bootfs_space
Expand Down Expand Up @@ -91,4 +93,43 @@ rm out.txt
assert_bootfs_has_n_bootcsum_dirs 2
assert_not_streq "$bootloader_orig" "$(sha256sum /boot/loader/entries/*)"

# This next test relies on the fact that FCOS currently uses ext4 for /boot.
# If that ever changes, we can reprovision boot to be ext4.
if [[ $(findmnt -no FSTYPE /boot) != ext4 ]]; then
assert_not_reached "/boot is not ext4"
fi

# Put modkernel2 in rollback position
rpm-ostree rollback

# Below, we test that a bootcsum dir sized below f_bfree but still large enough
# to not actually fit (because some filesystems like ext4 include reserved
# overhead in their f_bfree count for some reason) will still trigger the auto-
# prune logic.

unconsume_bootfs_space

# Size the bigfile just right so that the kernel+initrd will be just at the max
# limit according to f_bfree.
unshare -m bash -c \
"mount -o rw,remount /boot && \
cp /usr/lib/modules/`uname -r`/{vmlinuz,initramfs.img} /boot"
free_blocks=$(stat --file-system /boot -c '%f')
unshare -m bash -c \
"mount -o rw,remount /boot && rm /boot/{vmlinuz,initramfs.img}"
consume_bootfs_space "$((free_blocks))"

rpm-ostree rebase :modkernel1
if ostree admin finalize-staged |& tee out.txt; then
assert_not_reached "successfully wrote kernel without auto-pruning"
fi
assert_file_has_content out.txt "No space left on device"
rm out.txt

# now, try again but with auto-pruning enabled
rpm-ostree rebase :modkernel1
OSTREE_SYSROOT_OPTS=early-prune ostree admin finalize-staged |& tee out.txt
assert_file_has_content out.txt "updating bootloader in two steps"
rm out.txt

echo "ok bootfs auto-prune"