]> git.ipfire.org Git - people/stevee/ipfire-2.x.git/commitdiff
btrfsctrl: New suid binary helper program btrfsctrl
authorStefan Schantl <stefan.schantl@ipfire.org>
Fri, 21 Mar 2025 20:01:30 +0000 (21:01 +0100)
committerStefan Schantl <stefan.schantl@ipfire.org>
Fri, 21 Mar 2025 20:01:30 +0000 (21:01 +0100)
This binary is used to show, create, delete and restory snapshots
on a BTRFS installed system.

Signed-off-by: Stefan Schantl <stefan.schantl@ipfire.org>
config/rootfiles/common/misc-progs
src/misc-progs/Makefile
src/misc-progs/btrfsctrl.c [new file with mode: 0644]

index d6594b3f8da1d1fb4dd7978dc71cac678289cb59..33cf82f2b4a54a75a8f6a7d69aa19d6872c6fe67 100644 (file)
@@ -1,5 +1,6 @@
 usr/local/bin/addonctrl
 usr/local/bin/backupctrl
+usr/local/bin/btrfscrtl
 usr/local/bin/captivectrl
 #usr/local/bin/clamavctrl
 usr/local/bin/collectdctrl
index 1ae12b2946b91899bb61bad042876b5a54d87691..610eb5aec02cad015f061fd94aa306cbc0ffa586 100644 (file)
@@ -32,7 +32,7 @@ SUID_PROGS = squidctrl sshctrl ipfirereboot \
        smartctrl clamavctrl addonctrl pakfire wlanapctrl \
        setaliases urlfilterctrl updxlratorctrl fireinfoctrl rebuildroutes \
        getconntracktable wirelessclient torctrl ddnsctrl unboundctrl \
-       captivectrl
+       captivectrl btrfsctrl
 
 OBJS = $(patsubst %,%.o,$(PROGS) $(SUID_PROGS))
 
@@ -55,3 +55,6 @@ setuid.o: setuid.c setuid.h
 
 $(PROGS) $(SUID_PROGS): setuid.o | $(OBJS)
        $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $@.o $< $(LIBS)
+
+btrfsctrl: btrfsctrl.o
+       $(CC) $(CFLAGS) $(LDFLAGS) -o $@ setuid.o $< $(LIBS) -lbtrfsutil
diff --git a/src/misc-progs/btrfsctrl.c b/src/misc-progs/btrfsctrl.c
new file mode 100644 (file)
index 0000000..016ecb9
--- /dev/null
@@ -0,0 +1,642 @@
+/* This file is part of the IPFire Firewall.
+ *
+ * This program is distributed under the terms of the GNU General Public
+ * Licence.  See the file COPYING for details.
+ *
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <mntent.h>
+#include <time.h>
+#include <libgen.h>
+#include "setuid.h"
+
+#include "btrfsutil.h"
+#include "initreq.h"
+
+#define BTRFSPROG "/usr/bin/btrfs"
+#define SNAPSHOTDIR "/.snapshots"
+#define ROOTDIR "/"
+#define RESTOREDIR "/tmp/restore"
+#define PROCMOUNTS "/proc/mounts"
+
+#define BACKUP_PREFIX "auto-moving-to"
+
+#define VALID_NAME_CHARS "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_"
+#define NUMBERS "0123456789"
+#define BTRFS_FS_TREE_OBJECTID 5
+
+/*
+ * _btrfsctrl_validate_name Function
+ *
+ * Validates if a given snapshot name only contains allowed characters.
+ *
+*/
+static int _btrfsctrl_validate_name (const char *name) {
+       // Check if the given snapshot name only contains allowed characters.
+        if( strspn(name, VALID_NAME_CHARS) != strlen(name)) {
+                return -1;
+        }
+
+       return 0;
+}
+
+/* 
+ * _btrfsctrl_validate_number Function
+ *
+ * Validates if a given number of type char only contains digits.
+ *
+*/
+static int _btrfsctrl_validate_number(const char *number) {
+       // Check if the given id is a valid number.
+       if( strspn(number, NUMBERS) != strlen(number)) {
+               return -1;
+       }
+
+       return 0;
+}
+
+/* 
+ * _btrfsctrl_convert_str_to_uint64_t Function
+ *
+ * Converts a number with type char into a unsigned long intager.
+ *
+ * In case a number has been passed as argv it's type is char, but  a lot of
+ * functions needs them as long int.
+ *
+*/
+static unsigned long int _btrfsctrl_convert_str_to_uint64_t(const char *number) {
+       // Convert the given number string to unsigned long int.
+       uint64_t long_uint = strtoul (number, NULL, 0);
+
+       return long_uint;
+}
+
+
+/* 
+ * btrfsctrl_create_snapshot Function
+ *
+ * Requires a given name and True/False to set if a the snapshot should be read-only.
+ *
+ */
+static int btrfsctrl_create_snapshot (const char *name, bool read_only) {
+       char destination[STRING_SIZE];
+       int ro_flag = 0;
+
+       // Set read-only flag if requested.
+       if(read_only == true) {
+               ro_flag = BTRFS_UTIL_CREATE_SNAPSHOT_READ_ONLY;
+       }
+
+       // Build destination directory.
+       snprintf(destination, sizeof(destination), "%s/%s", SNAPSHOTDIR, name);
+
+       // Create the snapshot in read-only mode.
+       int ret = btrfs_util_subvolume_snapshot(ROOTDIR, destination, ro_flag, NULL, NULL);
+       if (ret != BTRFS_UTIL_OK) {
+               // Get the error message.
+               const char *error = btrfs_util_strerror(ret);
+               fprintf(stderr, "Could not create snapshot: %s\n", error);
+               return -1;
+       }
+
+       return 0;
+}
+
+/*
+ * _btrfsctrl_create_directory Function
+ *
+ * Function to create the defined RESTOREDIR in case it does not exist yet.
+ *
+*/
+static int _btrfsctrl_create_directory(const char *directory) {
+       // Try to access the directory.
+       if (access(RESTOREDIR, X_OK) != 0) {
+               // Create the directory.
+               int ret = mkdir(directory, S_IRWXU|S_IRWXG|S_IRWXO);
+
+               // Handle error code.
+               if (ret != 0 && errno != EEXIST) {
+                       return -1;
+               }
+       }
+
+       return 0;
+}
+
+/*
+ * _btrfsctrl_get_root_device Function.
+ *
+ * This function will return the device where root is currently mounted to.
+ *
+*/
+
+static const char * _btrfsctrl_get_root_device () {
+       struct mntent *ent;
+       FILE *mounts;
+
+       // Open kernel proc filesystem.
+       mounts = setmntent(PROCMOUNTS, "r");
+               if (mounts == NULL) {
+                       return false;
+               }
+
+       // Loop through all known mount points.
+       while (NULL != (ent = getmntent(mounts))) {
+               // Check if the current processed mount point is the root filesystem.
+               if (strcmp(ent->mnt_dir, ROOTDIR) == 0) {
+                       break;
+               }
+       }
+
+       // Close proc filesystem.
+       endmntent(mounts);
+
+       return ent->mnt_fsname;
+}
+
+/*
+ * _btrfsctrl_mount_btrfs_lvl5 Function.
+ *
+ * Function to mount the top level of a BTRFS.
+ *
+*/
+static int _btrfsctrl_mount_btrfs_lvl5(const char *device) {
+       char mount_options[STRING_SIZE];
+
+       // Assign mount options.
+       snprintf(mount_options, sizeof(mount_options), "subvolid=%d", BTRFS_FS_TREE_OBJECTID);
+
+       // Mount the top level BTRFS filesystem.
+        int ret = mount(device, RESTOREDIR, "btrfs", 0, mount_options);
+       if (ret < 0) {
+               return -1;
+       }
+
+       return 0;
+}
+
+/*
+ * _btrfsctrl_umount_btrfs_lvl5 Function
+ *
+ * Umounts a mounted top level BTRFS from the defined restore directory.
+ *
+*/
+
+static int _btrfsctrl_umount_btrfs_lvl5() {
+       int retry = 1;
+       int counter = 0;
+
+       while(1) {
+               // Reset the retry marker
+               retry = 0;
+
+               // Try to umount the filesystem.
+               int ret = umount2(RESTOREDIR, 0);
+               if (ret) {
+                       switch(errno) {
+                               case EBUSY:
+                                       // Set marker to retry the umount.
+                                       retry = 1;
+
+                                       // Ignore if the rootfs could not be unmounted yet,
+                                       // because it is still used.
+                                       break;
+                               case EINVAL:
+                                       // Ignore if the rootfs already has been unmounted
+                                       break;
+                               case ENOENT:
+                                       // Ignore if the directory does not longer exist.
+                                       break;
+                               default:
+                                       return ret;
+                                }
+                        }
+
+               // Abort loop if the rootfs got umounted
+               if (retry == 0) {
+                       return 0;
+               }
+
+               // Abort after five failed umount attempts
+               if (counter == 5) {
+                       return -1;
+               }
+
+               // Increment counter.
+               counter++;
+       }
+}
+
+/*
+ * _btrfsctrl_generate_backup_name Function
+ *
+ * Function which generates and returns a backup name for the old root
+ * when booting into or restoring a snapshot.
+ *
+*/
+
+static const char * _btrfsctrl_generate_backup_name (const char *path) {
+       time_t my_time;
+       struct tm * timeinfo;
+       char *backup_name = NULL;
+       char *pathc = NULL;
+       char *bname = NULL;
+
+       // Use basename function to get the name of the given path.
+       pathc = strdup(path);
+       bname = basename(pathc);
+
+       // Get the current time.
+       time (&my_time);
+       timeinfo = localtime (&my_time);
+
+       // Generate name for backup of the current rootdir.
+       asprintf(&backup_name, "@snapshots/%d-%02d-%02d-%02d-%02d-%s-%s", timeinfo->tm_year+1900, timeinfo->tm_mon+1, timeinfo->tm_mday,
+                                timeinfo->tm_hour, timeinfo->tm_min, BACKUP_PREFIX, bname);
+
+       return backup_name;
+}
+
+/*
+ * _btrfsctrl_generate_abs_backup_path Function
+ *
+ * Function which will require a path as input and will return the absolute path from defined backupdir.
+ *
+*/
+static const char * _btrfsctrl_generate_abs_path (const char *prefix, const char *path) {
+       char *abs_path = NULL;
+
+       //Generate absolute path.
+       asprintf(&abs_path, "%s/\%s", prefix, path);
+
+       return abs_path;
+}
+
+/*
+ * _btrfsctrl_call_for_reboot Function
+ *
+ * This functions uses the sysvinit FIFO to talk to initd and request for a reboot.
+ *
+*/
+static int _btrfsctrl_call_for_reboot() {
+       struct init_request request;
+       int fd;
+
+       // Allocate some memory for the request.
+       memset(&request, 0, sizeof(request));
+
+       // Define the request to change to runlevel six (reboot)
+       request.magic = INIT_MAGIC;
+       request.cmd = INIT_CMD_RUNLVL;
+       request.runlevel = '6';
+
+       // Open the FIFO pipe to the init process and send the request.
+       if ((fd = open(INIT_FIFO, O_WRONLY)) >= 0) {
+               ssize_t p = 0;
+               size_t s  = sizeof(request);
+               void *ptr = &request;
+
+               while (s > 0) {
+                       p = write(fd, ptr, s);
+                       if (p < 0) {
+                               if (errno == EINTR || errno == EAGAIN)
+                                       continue;
+                               break;
+                       }
+                       ptr += p;
+                       s -= p;
+               }
+               close(fd);
+       }
+
+       return 0;
+
+}
+
+int main(int argc, char *argv[]) {
+       char command[STRING_SIZE];
+
+       if (!(initsetuid()))
+               exit(1);
+
+       if (argc < 2) {
+               fprintf(stderr, "\nNo argument given.\n\nbtrfsctrl (filesystem-usage|subvolume-list|snapshot-create|snapshot-delete|snapshot-restore)\n\n");
+               exit(1);
+       }
+
+       if (strcmp(argv[1], "filesystem-usage") == 0) {
+               snprintf(command, sizeof(command), BTRFSPROG " filesystem usage -b %s", ROOTDIR);
+               safe_system(command);
+
+       // Get a list of all known subvolumes.
+       } else if (strcmp(argv[1], "subvolume-list") == 0) {
+               struct btrfs_util_subvolume_iterator* subvolume_iterator = NULL;
+               struct btrfs_util_subvolume_info subvolume_info;
+               char *path = NULL;
+               int ret;
+
+               // Create new subvolume iterator. 
+               ret = btrfs_util_subvolume_iter_create(ROOTDIR, BTRFS_FS_TREE_OBJECTID, 0, &subvolume_iterator);
+               if (ret != BTRFS_UTIL_OK) {
+                       // Get the error message.
+                       const char *error = btrfs_util_strerror(ret);
+                       fprintf(stderr, "Could not create subvolumr iterator.\n%s\n", error);
+                       exit(1);
+               }
+
+               // Init and iterate over the first subvolume.
+               ret = btrfs_util_subvolume_iter_next_info(subvolume_iterator, &path, &subvolume_info);
+               if(ret != BTRFS_UTIL_OK) {
+                       // Get the error message.
+                       const char *error = btrfs_util_strerror(ret);
+                       fprintf(stderr, "Could not iterate over the first subvolume.\n%s\n", error);
+
+                       // Destroy the subvolume iterator.
+                       btrfs_util_subvolume_iter_destroy(subvolume_iterator);
+
+                       // Free the path, if set.
+                       if (path != NULL) {
+                               free(path);
+                       }
+
+                       exit(1);
+               }
+
+               // Iterate over the existing subvolumes/snapshots.
+               while(path) {
+                       // Generate the line to print.
+                       printf("%ld %s %ld\n", subvolume_info.id, path, subvolume_info.parent_id);
+
+                       // Free the path, if set.
+                       if (path != NULL) {
+                               free(path);
+                       }
+
+                       // Iterate over the next subvolume.
+                       ret = btrfs_util_subvolume_iter_next_info(subvolume_iterator, &path, &subvolume_info);
+
+                       // Handle return value.
+                       if (ret == BTRFS_UTIL_ERROR_STOP_ITERATION) {
+                               // Nothing more to iterate.
+                               break;
+
+                       } else if ( ret != BTRFS_UTIL_OK) {
+                               // Get the error message.
+                               const char *error = btrfs_util_strerror(ret);
+
+                               fprintf(stderr, "Could not iterate over the next subvolume. \n%s\n", error);
+
+                               // Destroy the subvolume iterator.
+                               btrfs_util_subvolume_iter_destroy(subvolume_iterator);
+
+                               // Free the path if set.
+                               if (path != NULL) {
+                                       free(path);
+                               }
+                               exit(1);
+                       }
+               }
+
+               // Destroy the subvolume iterator.
+               btrfs_util_subvolume_iter_destroy(subvolume_iterator);
+
+       // Create a snapshot.
+       } else if (strcmp(argv[1], "snapshot-create") == 0) {
+               int r;
+
+               // Check if the given snapshot name is valid.
+               r = _btrfsctrl_validate_name(argv[2]);
+               if (r < 0) {
+                       fprintf(stderr, "\nInvalid snapshot name. \nValid characters are: %s\n\n", VALID_NAME_CHARS);
+                       exit(1);
+               }
+
+               // Call function and create a read-only snapshot with the given name.
+               r = btrfsctrl_create_snapshot(argv[2], true);
+               if (r < 0) {
+                       exit(1);
+               }
+
+       // Delete a snapshot by it's given ID
+       } else if (strcmp(argv[1], "snapshot-delete") == 0) {
+               int ret;
+
+               // Check if the given id is a valid number.
+               ret = _btrfsctrl_validate_number(argv[2]);
+               if (ret < 0) { 
+                       fprintf(stderr, "\n Invalid snaphot ID: Not a numerical input.\n\n");
+                       exit(1);
+               }
+
+               // Convert the given id string to unsigned long int.
+               uint64_t id = _btrfsctrl_convert_str_to_uint64_t(argv[2]);
+
+               // Open a file descriptor to the SNAPSHOTDIR.
+               int fd = open(SNAPSHOTDIR, O_DIRECTORY);
+               if (fd < 0) {
+                       fprintf(stderr, "\nCould not open %s - Code: %d\n", SNAPSHOTDIR, fd);
+                       exit(1);
+               }
+
+               // Delte the snapshot with the given ID.
+               ret = btrfs_util_delete_subvolume_by_id_fd(fd, id);
+               if (ret != BTRFS_UTIL_OK) {
+                       // Get error message.
+                       const char *error = btrfs_util_strerror(ret);
+                       fprintf(stderr, "%s\n", error);
+
+                       // Close file descriptor.
+                       close(fd);
+                       exit(1);
+               }
+
+               // Close file desriptor.
+               close(fd);
+
+       // Restore a snapshot with a given ID.
+       } else if (strcmp(argv[1], "snapshot-restore") == 0) {
+               int ret;
+               char *path = NULL;
+               bool mounted = false;
+
+               // Check if the given id is a valid number.
+               ret = _btrfsctrl_validate_number(argv[2]);
+               if(ret < 0) {
+                       fprintf(stderr, "\n Invalid snapshot ID. Not a numerical input.\n\n");
+
+                       goto ERROR;
+               }
+
+               // Convert the given id string to unsigned long int.
+               uint64_t id = _btrfsctrl_convert_str_to_uint64_t(argv[2]);
+
+               // Get the path of the given snapshot ID.
+               ret = btrfs_util_subvolume_get_path(ROOTDIR, id, &path);
+               if (ret != BTRFS_UTIL_OK) {
+                       // Get error message.
+                       const char *error = btrfs_util_strerror(ret);
+                       fprintf(stderr, "%s\n", error);
+
+                       goto ERROR;
+               }
+
+               // Create restore directory.
+               ret = _btrfsctrl_create_directory(RESTOREDIR);
+               if (ret < 0) {
+                       // Get the error message.
+                       const char *error = strerror(errno);
+                       fprintf(stderr, "Could not create %s %s\n", RESTOREDIR, error);
+
+                       goto ERROR;
+               }
+
+               // Get the device name of the mounted root partition.
+               const char *rootdev = _btrfsctrl_get_root_device();
+               if (!rootdev) {
+                       fprintf(stderr, "Could not get root device.\n");
+
+                       goto ERROR;
+               }
+
+               // Mount the top level of the BTRFS.
+               ret = _btrfsctrl_mount_btrfs_lvl5(rootdev);
+               if (ret < 0) {
+                       // Get error message.
+                       const char *error = strerror(errno);
+                       fprintf(stderr, "Could not mount top level BTRFS to %s. %s\n", RESTOREDIR, error);
+
+                       goto ERROR;
+               }
+
+               // Set mounted to true.
+               mounted = true;
+
+               // Generate a backup name.
+               const char *backup_name = _btrfsctrl_generate_backup_name(path);
+               if (!backup_name) {
+                       fprintf(stderr, "Could not generate a backup name.\n");
+
+                       goto ERROR;
+               }
+
+               // Generate absolute path values.
+               const char *oldname = _btrfsctrl_generate_abs_path(RESTOREDIR, "@");
+               const char *newname = _btrfsctrl_generate_abs_path(RESTOREDIR, backup_name);
+               if ((!oldname) || (!newname)) {
+                       fprintf(stderr, "Could not generate absolute path values.\n");
+
+                       goto ERROR;
+               }
+
+               // Rename / Move the current @ to backup location
+               ret = rename(oldname, newname);
+               if (ret < 0) {
+                       if (errno == ENOENT) {
+                               fprintf(stderr, "ENOENT\n");
+                       }
+
+                       // Get error message.
+                       const char *error = strerror(errno);
+                       fprintf(stderr, "Could not backup/move current root filesystem. %s\n", error);
+                       
+                       goto ERROR;
+               }
+
+               /*
+                * XXX
+                *
+                * Needs some more attention. When swithing the old/current root to read-only no write
+                * operations are possible. So the RESTOREDIT could not be deleted correctly afterwards
+                * and also during init reboot the services can not write anything back to disk.
+                */
+               // Set the backup of root dir to read-only.
+               /*
+               ret = btrfs_util_subvolume_set_read_only(newname, true);
+               if (ret != BTRFS_UTIL_OK) {
+                       // Get error message.
+                       const char *error = btrfs_util_strerror(ret);
+                       fprintf(stderr, "Could not change the rootfs backup to read-only. %s\n", error);
+
+                       goto ERROR;
+               }
+               */
+
+               // Create a snapshot of the given snapshot ID as the new root.
+               const char *restore = _btrfsctrl_generate_abs_path(RESTOREDIR, path);
+                ret = btrfs_util_subvolume_snapshot(restore, oldname, 0, NULL, NULL);
+                if (ret > 0) {
+                        // Get the error message.
+                        const char *error = btrfs_util_strerror(ret);
+                        fprintf(stderr, "Could not restore snapshot %s: %s\n", path, error);
+
+                       goto ERROR;
+                }
+               
+
+
+               // Call sync to sync the disks
+               sync();
+
+               // Umount the BTRFS root filesystem from restore directory.
+               ret = _btrfsctrl_umount_btrfs_lvl5();
+               if (ret < 0) {
+                       // Get error message
+                       const char *error = strerror(errno);
+                       fprintf(stderr, "Could not umount BTRFS root filesystem from %s. %s\n", RESTOREDIR, error);
+
+                       goto ERROR;
+               }
+
+               // Set mounted to false again.
+               mounted = false;
+
+               // Remove the restore directory.
+               ret = rmdir(RESTOREDIR);
+               if(ret != 0) {
+                       fprintf(stderr, "Could not remove %s. %d\n", RESTOREDIR, ret);
+
+                       goto ERROR;
+               }
+
+               // Reboot the system
+               ret = _btrfsctrl_call_for_reboot();
+               if (ret < 0) {
+                       fprintf(stderr, "Could not reboot the system.\n");
+                       exit(1);
+               }
+
+               exit(0);
+
+               ERROR:
+                       // Umount the top root BTRFS in case it is mounted.
+                       if (mounted == true) {
+                               ret = _btrfsctrl_umount_btrfs_lvl5();
+                               if (ret < 0) {
+                                       // Get error message
+                                       const char *error = strerror(errno);
+                                       fprintf(stderr, "Could not umount BTRFS root filesystem during cleanup. %s\n", error);
+                               }
+                       }
+
+                       // Free path if set.
+                       if(path != NULL) {
+                               free(path);
+                       }
+
+                       exit(1);
+       } else {
+               fprintf(stderr, "\nBad argument given.\n\nbtrfsctrl (filesystem-usage|subvolume-list|snapshot-create|snapshot-delete|snapshot-restore)\n\n");
+               exit(1);
+       }
+
+       return 0;
+}