diff mbox series

[V2,10/10] btrfs: introduce support for snapshot with btrfs receive

Message ID 20241030163957.2822282-11-stefano.babic@swupdate.org
State Accepted
Headers show
Series Introduce BTRFS Snapshot Handler | expand

Commit Message

Stefano Babic Oct. 30, 2024, 4:39 p.m. UTC
Add a handler that supports snapshots created by "btrfs send" as
artifact stored into the SWU. The handler needs that btrfs utility is
installed on the system, and reports a runtime error if it is not found.
It is implemented as image handler, and the image is streamed to a
forked process that will run "btrfs receive" in background.

To realize this, a generic way to stream an artifact to an external
command is implemented. This allows to push the stream from the
copyimage() to an external command that accept the data from stdin, like
"btrfs receive" is doing. To extend this, a generic "executor" handler
is provided, too, that allows to set the external command as one of its
property.

Signed-off-by: Stefano Babic <stefano.babic@swupdate.org>
---
 doc/source/handlers.rst    |  54 +++++++++++++++++-
 handlers/btrfs_handler.c   | 112 +++++++++++++++++++++++++++++--------
 handlers/handler_helpers.c | 107 ++++++++++++++++++++++++++++++++++-
 include/handler_helpers.h  |   9 ++-
 4 files changed, 253 insertions(+), 29 deletions(-)

--
2.34.1
diff mbox series

Patch

diff --git a/doc/source/handlers.rst b/doc/source/handlers.rst
index f464998c..45b93e7b 100644
--- a/doc/source/handlers.rst
+++ b/doc/source/handlers.rst
@@ -1278,8 +1278,8 @@  found on the device. It is a partition handler and it runs before any image is i
 		}
 	});

-BTRFS Handler
--------------
+BTRFS Partition Handler
+-----------------------

 This handler is activated if support for BTRFS is on. It allows to created and delete subvolumes
 during an update.
@@ -1299,9 +1299,57 @@  during an update.
 	})


-If `mount` is set, SWUpdate will mount the device and the path is appenden to the
+If `mount` is set, SWUpdate will mount the device and the path is appended to the
 mountpoint used with mount. If device is already mounted, path is the absolute path.

+BTRFS Snapshot Handler
+----------------------
+
+The handler allows to install a BTRFS snapshot created with the "btrfs send" command.
+SWUpdate is using the external "btrfs" utility, that must be installed on the target,
+and "btrfs receive" is executed by sending the stream to the command.
+
+All generic features are avaiulable, that means that an srtifact can be streamed by
+using the "installed-directly" attribute.
+
+
+::
+
+        images: (
+	{
+		filename = "btrfs-snapshot";
+		type = "btrfs-receive";
+		device = <optional, device with BTRFS fs, must be set if tomount is "true">;
+		properties: {
+			path = <mandatory, path where to install subvolume>;
+			btrfs-cmd = <optional, path to btrfs command>;
+                        tomount = <boolean, optional, "true" / "false" >;
+		}
+	})
+
+If `tomount` is set, SWUpdate will temporary mount "device" as BTRFS filesystem and will try to install
+the snapshot to `path`. `btrfs-cmd` is optional, fallback is /usr/bin/btrfs.
+
+Generic Executor handler
+------------------------
+
+The BTRFS snapshot handler requires to stream an artifact after normal handling
+(decompression, decryption, etc.) to the external command "btrfs" without any temporary copy.
+The same infrastucture can be used to stream any artifact to any arbitrary external command
+that accepts the stream as stdin. This is done with the "executor" handler.
+
+
+::
+
+		{
+			filename = "test";
+			type = "executor";
+			properties: {
+				cmd = <mandatory, command to be executed in a pipe>;
+			}
+		}
+
+
 Delta Update Handler
 --------------------

diff --git a/handlers/btrfs_handler.c b/handlers/btrfs_handler.c
index e7470a7e..aea3431e 100644
--- a/handlers/btrfs_handler.c
+++ b/handlers/btrfs_handler.c
@@ -7,13 +7,16 @@ 

 #include <errno.h>
 #include <stdio.h>
-#include <util.h>
 #include <unistd.h>
 #include <blkid/blkid.h>
+#include <pthread.h>
 #include <btrfsutil.h>
 #include "progress.h"
 #include "swupdate_image.h"
 #include "handler.h"
+#include <util.h>
+#include <pctl.h>
+#include "handler_helpers.h"

 typedef enum {
 	BTRFS_UNKNOWN,
@@ -21,7 +24,34 @@  typedef enum {
 	BTRFS_DELETE_SUBVOLUME
 } btrfs_op_t;

-void btrfs_handler(void);
+#define BTRFS_MOUNT(mount, device, mnt, subvol, path) do {  \
+	if (mount) { \
+		if (!btrfs_mount(device, &mnt)) { \
+			ERROR("%s cannot be mounted with btrfs", device); \
+			return -1; \
+		} \
+		path = swupdate_strcat(2, mnt, subvol); \
+	} else \
+		path = strdup(subvol); \
+} while (0)
+
+/*
+ * This is just a wrapper as some parms are fixed
+ * (filesystem, etc)
+ */
+static bool btrfs_mount(const char *device, char **mnt)
+{
+	if (!mnt)
+		return false;
+
+	*mnt = swupdate_temporary_mount(MNT_DATA, device, "btrfs");
+	if (!*mnt) {
+		ERROR("%s cannot be mounted with btrfs", device);
+		return false;
+	}
+
+	return true;
+}

 static int btrfs(struct img_type *img,
 		      void __attribute__ ((__unused__)) *data)
@@ -32,7 +62,8 @@  static int btrfs(struct img_type *img,

 	char *subvol_path = dict_get_value(&img->properties, "path");
 	char *cmd = dict_get_value(&img->properties, "command");
-	char *globalpath;
+	bool tomount = strtobool(dict_get_value(&img->properties, "mount"));
+	char *globalpath = NULL;
 	char *mountpoint = NULL;

 	op = IS_STR_EQUAL(cmd, "create") ? BTRFS_CREATE_SUBVOLUME :
@@ -42,22 +73,8 @@  static int btrfs(struct img_type *img,
 		ERROR("Wrong operation of btrfs filesystem: %s", cmd);
 		return -EINVAL;
 	}
-	bool tomount = strtobool(dict_get_value(&img->properties, "mount"));
-	if (tomount) {
-		if (!strlen(img->device)) {
-			ERROR("btrfs must be mounted, no device set");
-			return -EINVAL;
-		}
-		DEBUG("Try to mount %s as BTRFS", mountpoint);
-		mountpoint = swupdate_temporary_mount(MNT_DATA, img->device, "btrfs");
-		if (!mountpoint) {
-			ERROR("%s cannot be mounted with btrfs", img->device);
-			return -1;
-		}
-		globalpath = alloca(strlen(mountpoint) + strlen(subvol_path) + 2);
-		globalpath = strcat(mountpoint, subvol_path);
-	} else
-		globalpath = subvol_path;
+
+	BTRFS_MOUNT(tomount, img->device, mountpoint, subvol_path, globalpath);

 	DEBUG("%s subvolume %s...", (op == BTRFS_CREATE_SUBVOLUME) ? "Creating" : "Deleting", subvol_path);
 	switch (op) {
@@ -78,27 +95,74 @@  static int btrfs(struct img_type *img,
 		ret = -1;
 	}

-	if (tomount) {
+	if (tomount && mountpoint) {
 		/*
 		 * btrfs needs some time after creating a subvolume,
 		 * so just delay here
 		 */
 		sleep(1);
-		if (mountpoint) {
-			swupdate_umount(mountpoint);
-		}
+		swupdate_umount(mountpoint);
 	}
 	/*
 	 * Declare that handler has finished
 	 */
 	swupdate_progress_update(100);

+	free(globalpath);
+
+	return ret;
+}
+
+static int install_btrfs_snapshot(struct img_type *img,
+		      void __attribute__ ((__unused__)) *data)
+{
+	int ret = 0;
+	struct bgtask_handle btrfs_handle;
+	char *subvol_path = dict_get_value(&img->properties, "path");
+	bool tomount = strtobool(dict_get_value(&img->properties, "mount"));
+	const char *btrfscmd = dict_get_value(&img->properties, "btrfs-cmd");
+	char *globalpath = NULL;
+	char *mountpoint = NULL;
+
+	/*
+	 * if no path for the command is found, the handler assumes that
+	 * btrfs is in standard path
+	 */
+	if (!btrfscmd)
+		btrfscmd = "/usr/bin/btrfs ";
+
+	BTRFS_MOUNT(tomount, img->device, mountpoint, subvol_path, globalpath);
+
+	btrfs_handle.cmd = btrfscmd;
+
+	/*
+	 * Note: btrfs tool writes to stderr instead of stdout
+	 * and SWUpdate will intercept and show the output as error
+	 * even if they are not. Just redirect it to stdout to drop
+	 * error messages
+	 */
+	btrfs_handle.parms = swupdate_strcat(3, " receive ", globalpath, " 2>&1");
+	free(globalpath);
+	btrfs_handle.img = img;
+
+	ret = bgtask_handler(&btrfs_handle);
+
+	if (tomount && mountpoint)
+		swupdate_umount(mountpoint);
+	free(btrfs_handle.parms);
 	return ret;
 }

 __attribute__((constructor))
-void btrfs_handler(void)
+static void btrfs_handler(void)
 {
 	register_handler("btrfs", btrfs,
 			 PARTITION_HANDLER | NO_DATA_HANDLER, NULL);
 }
+
+__attribute__((constructor))
+static void btrfs_receive_handler(void)
+{
+	register_handler("btrfs-receive", install_btrfs_snapshot,
+				IMAGE_HANDLER, NULL);
+}
diff --git a/handlers/handler_helpers.c b/handlers/handler_helpers.c
index 77467d39..2100be62 100644
--- a/handlers/handler_helpers.c
+++ b/handlers/handler_helpers.c
@@ -1,10 +1,11 @@ 
 /*
- * (C) Copyright 2022
+ * Copyright (C) 2024
  * Stefano Babic, stefano.babic@swupdate.org.
  *
  * SPDX-License-Identifier:     GPL-2.0-only
  */

+#include <errno.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/stat.h>
@@ -13,12 +14,34 @@ 
 #include <sys/ioctl.h>
 #include <fcntl.h>
 #include <pthread.h>
+#include <signal.h>
 #include <unistd.h>
 #include "handler_helpers.h"
 #include "installer.h"
+#include "swupdate_image.h"
 #include "pctl.h"
 #include "util.h"

+struct thread_handle {
+	struct hnd_load_priv *hndtransfer;
+	struct img_type *img;
+};
+
+static void *copyimage_thread(void *p)
+{
+	struct thread_handle *hnd = (struct thread_handle *)p;
+	struct hnd_load_priv *priv = hnd->hndtransfer;
+	int ret = 0;
+
+	ret = copyimage(priv, hnd->img, handler_transfer_data);
+
+	if (ret) {
+		ERROR("Transferring image was not successful");
+	}
+	close(priv->fifo[FIFO_HND_WRITE]);
+	pthread_exit((void*)(intptr_t)ret);
+}
+
 /*
  * This is the copyimage's callback. When called,
  * there is a buffer to be passed to curl connections
@@ -73,4 +96,86 @@  void *chain_handler_thread(void *data)
 	return (void *)ret;
 }

+/*
+ * This receive a command from a handler as external command that must be executed.
+ * It performs the following:
+ * - creates pipes for internal IPC
+ * - creates a thread that run (fork) the command
+ * - execute copyimage with callback that write into the FIFO
+ * - wait until thread exits and returns the result
+ */
+int bgtask_handler(struct bgtask_handle *bg)
+{
+	int ret = 0;
+	struct hnd_load_priv data_handle;
+	struct thread_handle hnd;
+	char *cmd;
+
+	/*
+	 * if no path for the command is found, the handler assumes that
+	 * btrfs is in standard path
+	 */
+	if (!bg->cmd || !bg->img)
+		return -EINVAL;
+
+	if (access(bg->cmd, X_OK)) {
+		ERROR("Handler requires %s, not found.", bg->cmd);
+		return -EINVAL;
+	}
+
+	/*
+	 * Create one FIFO for each connection to be thread safe
+	 */
+	if (pipe(data_handle.fifo) < 0) {
+		ERROR("Cannot create internal pipes, exit..");
+		return -EFAULT;
+	}
+
+	hnd.hndtransfer = &data_handle;
+	hnd.img = bg->img;
+	cmd = swupdate_strcat(3, bg->cmd, " ", bg->parms);
+
+	/*
+	 * Starts a backgroud task to fill in the
+	 * FIFO using copyimage
+	 */
+	start_thread(copyimage_thread, &hnd);
+
+	/*
+	 * Start to write in the FIFO - vene if the bg process is not started.
+	 * If the FIFO becomes full, this thread will be paused until the bg
+	 * process (and btrfs receive) will start to consume the data
+	 */
+
+	ret = run_system_cmd_with_fdin(cmd, data_handle.fifo);
+	free(cmd);
+	return ret;
+}
+
+static int generic_executor(struct img_type *img,
+		      void __attribute__ ((__unused__)) *data)
+{
+	int ret = 0;
+	struct bgtask_handle handle;
+	const char *cmd = dict_get_value(&img->properties, "cmd");
+
+	if (!cmd) {
+		ERROR("No cmd set, add cmd property");
+		return -EINVAL;
+	}
+
+	handle.cmd = cmd;
+	handle.parms = dict_get_value(&img->properties, "parms");
+	handle.img = img;
+
+	ret = bgtask_handler(&handle);

+	return ret;
+}
+
+__attribute__((constructor))
+static void executor_handler(void)
+{
+	register_handler("executor", generic_executor,
+				IMAGE_HANDLER, NULL);
+}
diff --git a/include/handler_helpers.h b/include/handler_helpers.h
index 30d338eb..17aed10b 100644
--- a/include/handler_helpers.h
+++ b/include/handler_helpers.h
@@ -21,5 +21,12 @@  struct hnd_load_priv {
 	int exit_status;
 };

-extern void *chain_handler_thread(void *data);
+struct bgtask_handle {
+	const char *cmd;
+	char *parms;
+	struct img_type *img;
+};
+
+void *chain_handler_thread(void *data);
 extern int handler_transfer_data(void *data, const void *buf, size_t len);
+int bgtask_handler(struct bgtask_handle *bg);