From bb90fbe4d5147dba65c13d5d2df919b38583b31a Mon Sep 17 00:00:00 2001 From: "Christian S.J. Peron" Date: Thu, 8 Oct 2026 12:03:57 -0500 Subject: [PATCH] Add remove image support --- README.md | 10 ++++ src/cblock/image.c | 50 ++++++++++++++++++- src/cblockd/exec.c | 6 +++ src/libcblock/vec.c | 3 +- src/shell/Makefile | 1 + src/shell/cmd_image_prune.sh | 14 +----- src/shell/cmd_image_remove.sh | 94 +++++++++++++++++++++++++++++++++++ src/shell/common.sh | 40 +++++++++++++++ 8 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 src/shell/cmd_image_remove.sh diff --git a/README.md b/README.md index 193f670..6efc74a 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,16 @@ forge latest 10.17M 2021-05-27 00:32:08 % ``` +To remove an image, give its name and tag (the tag defaults to `latest`): +``` +% sudo cblock images --remove freebsd-13_4:latest +Removed freebsd-13_4:latest +% +``` +If the image has other tags, only this tag is removed. The image itself is +removed with its last tag, and cblockd refuses to remove an image that a +running cellblock or build is using. + ### Launching your Cellblock Now we are ready to launch the container. Note with `--host-networking` the cblock daemon diff --git a/src/cblock/image.c b/src/cblock/image.c index 48a555b..8f913bb 100644 --- a/src/cblock/image.c +++ b/src/cblock/image.c @@ -43,12 +43,14 @@ struct image_config { int i_quiet; int i_do_prune; + int i_do_remove; }; static struct option image_options[] = { { "help", no_argument, 0, 'h' }, { "quiet", no_argument, 0, 'q' }, { "prune", no_argument, 0, 'p' }, + { "remove", no_argument, 0, 'r' }, { 0, 0, 0, 0 } }; @@ -56,10 +58,13 @@ static void image_usage(void) { (void) fprintf(stderr, - "Usage: cblock images [OPTIONS]\n\n" + "Usage: cblock images [OPTIONS]\n" + " cblock images --remove NAME[:TAG] ...\n\n" "Options\n" " -h, --help Print help\n" " -p, --prune Remove stopped/dead images\n" + " -r, --remove Remove image tags, and each image\n" + " with its last tag unless it is in use\n" " -q, --quiet Do not print column headers\n"); exit(1); } @@ -78,6 +83,37 @@ image_prune(struct image_config *icp __attribute__((unused)), int ctlsock) sock_ipc_from_sock_to_tty(ctlsock); } +static void +image_remove(int argc, char *argv[], int ctlsock) +{ + struct cblock_generic_command arg; + char *marshalled; + vec_t *vec; + int k; + + vec = vec_init(argc + 1); + if (vec == NULL) { + err(1, "vec_init failed"); + } + for (k = 0; k < argc; k++) { + vec_append(vec, argv[k]); + } + vec_finalize(vec); + marshalled = vec_marshal(vec); + if (marshalled == NULL) { + err(1, "vec_marshal failed"); + } + bzero(&arg, sizeof(arg)); + snprintf(arg.p_cmdname, sizeof(arg.p_cmdname), "image_remove"); + arg.p_mlen = vec->vec_marshalled_len; + if (sock_ipc_write_u32(ctlsock, PRISON_IPC_GENERIC_COMMAND) == -1 || + proto_send_generic_command(ctlsock, &arg, marshalled) == -1) { + errx(1, "failed to send image request"); + } + vec_free(vec); + sock_ipc_from_sock_to_tty(ctlsock); +} + static void image_get(struct image_config *icp __attribute__((unused)), int ctlsock) { @@ -102,7 +138,7 @@ image_main(int argc, char *argv [], int ctlsock) reset_getopt_state(); while (1) { option_index = 0; - c = getopt_long(argc, argv, "qhp", image_options, + c = getopt_long(argc, argv, "qhpr", image_options, &option_index); if (c == -1) { break; @@ -111,6 +147,9 @@ image_main(int argc, char *argv [], int ctlsock) case 'p': ic.i_do_prune = 1; break; + case 'r': + ic.i_do_remove = 1; + break; case 'q': ic.i_quiet = 1; break; @@ -124,6 +163,13 @@ image_main(int argc, char *argv [], int ctlsock) } argc -= optind; argv += optind; + if (ic.i_do_remove) { + if (argc == 0) { + image_usage(); + } + image_remove(argc, argv, ctlsock); + return (0); + } if (ic.i_do_prune) { image_prune(&ic, ctlsock); return (0); diff --git a/src/cblockd/exec.c b/src/cblockd/exec.c index ca9f35c..6c625fd 100644 --- a/src/cblockd/exec.c +++ b/src/cblockd/exec.c @@ -55,6 +55,7 @@ static struct command_ent command_list[] = { { "instance_prune", "cmd_instance_prune.sh" }, { "image_prune", "cmd_image_prune.sh" }, { "image_list", "cmd_image.sh" }, + { "image_remove", "cmd_image_remove.sh" }, { NULL, NULL } }; @@ -128,6 +129,11 @@ dispatch_generic_command(int sock) vec_append(cmd_vec, script_path); vec_append(cmd_vec, "-R"); vec_append(cmd_vec, gcfg.c_data_dir); + /* + * The client's arguments follow "--" so the script can't + * take any of them as an option, such as another -R. + */ + vec_append(cmd_vec, "--"); if (vec_merge(vec, cmd_vec) != 0) { err(1, "error argv vectors: HINT: increase cmd_vec"); } diff --git a/src/libcblock/vec.c b/src/libcblock/vec.c index b537d55..52ede95 100644 --- a/src/libcblock/vec.c +++ b/src/libcblock/vec.c @@ -172,7 +172,8 @@ vec_merge(vec_t *from, vec_t *to) { int k; - if ((from->vec_used + to->vec_used) > to->vec_alloc) { + /* Leave room for the NULL that vec_finalize() adds. */ + if ((from->vec_used + to->vec_used) >= to->vec_alloc) { fprintf(stderr, "vec_append: overflow\n"); return (VEC_OVERFLOW); } diff --git a/src/shell/Makefile b/src/shell/Makefile index 47a06a4..139588d 100644 --- a/src/shell/Makefile +++ b/src/shell/Makefile @@ -1,4 +1,5 @@ TARGETS=cmd_image.sh \ + cmd_image_remove.sh \ cmd_image_prune.sh \ cmd_instance_prune.sh \ common.sh \ diff --git a/src/shell/cmd_image_prune.sh b/src/shell/cmd_image_prune.sh index ab5f13a..5fa67e4 100755 --- a/src/shell/cmd_image_prune.sh +++ b/src/shell/cmd_image_prune.sh @@ -49,19 +49,7 @@ do_image_purge() continue fi printf "Removing un-referenced image: %s\n" $(basename $image) - case $CBLOCK_FS in - zfs) - vol=$(path_to_vol $image) - zfs destroy -r "$vol" - rm -fr "$vol" - ;; - ufs) - chflags -R noschg "$image" - rm -Wfr "$image" - ;; - *) - echo "No match on CBLOCK_FS" - esac + remove_image "$image" done } diff --git a/src/shell/cmd_image_remove.sh b/src/shell/cmd_image_remove.sh new file mode 100644 index 0000000..bcd6fb1 --- /dev/null +++ b/src/shell/cmd_image_remove.sh @@ -0,0 +1,94 @@ +#!/bin/sh +# +# Copyright (c) 2026 Christian S.J. Peron +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +# SUCH DAMAGE. +# +# Remove image tags: cmd_image_remove.sh -R DATA_DIR -- NAME[:TAG] ... +# A missing tag means "latest". The image itself is deleted with its +# last tag, unless a running instance or build is using it. +# +. "$(dirname "$0")/common.sh" +data_dir="" + +remove_tag() +{ + name="${1%%:*}" + tag="latest" + case "$1" in + *:*) + tag="${1#*:}" + ;; + esac + # The names come from the client. Keep them inside the images + # directory. + case "$name:$tag" in + -*|*/*|:*|*:|*:*:*) + echo "${1}: invalid image name" + return 1 + ;; + esac + link="${data_dir}/images/${name}:${tag}" + if [ ! -h "$link" ]; then + echo "${name}:${tag}: no such image" + return 1 + fi + image=$(readlink "$link") + for other in $(symlinks); do + if [ "$other" != "$link" ] && [ "$(readlink "$other")" = "$image" ]; then + rm "$link" + echo "Untagged ${name}:${tag}" + return 0 + fi + done + if image_in_use "$image"; then + echo "${name}:${tag}: image is in use, not removed" + return 1 + fi + rm "$link" + remove_image "$image" || return 1 + echo "Removed ${name}:${tag}" +} + +while getopts "R:" opt; do + case $opt in + R) + data_dir="$OPTARG" + ;; + *) + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +if [ ! "$data_dir" ]; then + echo "Must specify cblock data directory -R" + exit 1 +fi + +status=0 +for arg in "$@"; do + remove_tag "$arg" || status=1 +done +exit $status diff --git a/src/shell/common.sh b/src/shell/common.sh index e76cf91..aa63db0 100644 --- a/src/shell/common.sh +++ b/src/shell/common.sh @@ -42,6 +42,46 @@ symlinks() -mindepth 1 -maxdepth 1 -type l } +# Delete an image directory (or dataset) under ${data_dir}/images. The +# caller makes sure no tag points at it and nothing is using it. +remove_image() +{ + case $CBLOCK_FS in + zfs) + zfs destroy -r "$(path_to_vol "$1")" + rm -fr "$1" + ;; + ufs) + chflags -R noschg "$1" + rm -fr "$1" + ;; + *) + echo "No match on CBLOCK_FS" + return 1 + ;; + esac +} + +# Succeeds if a running instance or build is using the image in $1. +# Launches and builds mount the image's root as the lower layer of a +# unionfs (ufs), or clone a snapshot of its dataset (zfs). +image_in_use() +{ + case $CBLOCK_FS in + zfs) + zfs list -H -o clones -t snapshot -d 1 "$(path_to_vol "$1")" | \ + grep -qv '^-$' + ;; + ufs) + mount -p | awk -v dir="$1/root" ' + { n = split($1, layer, ":") + for (i = 1; i <= n; i++) + if (layer[i] == dir) found = 1 } + END { exit !found }' + ;; + esac +} + ip_to_int() { echo "$1" | awk -F. '{ print (($1 * 256 + $2) * 256 + $3) * 256 + $4 }'