diff --git a/.gitignore b/.gitignore index 7397e25..b0325a3 100644 --- a/.gitignore +++ b/.gitignore @@ -34,9 +34,11 @@ test-driver test-suite.log check-output.* tukit.pc +tukitd sbin/transactional-update src/transactional-update systemd/transactional-update.service +systemd/transactional-update-cleanup.service man/transactional-update.8 man/transactional-update.service.8 man/transactional-update.timer.8 diff --git a/Makefile.am b/Makefile.am index 774b587..bd9dcdc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,7 +5,7 @@ # AUTOMAKE_OPTIONS = 1.6 foreign check-news dist-xz # -SUBDIRS = lib tukit sbin man systemd logrotate dracut doc etc +SUBDIRS = lib tukit dbus sbin man systemd logrotate dracut doc etc CLEANFILES = *~ tukit.pc diff --git a/NEWS b/NEWS index 108b105..df2b0cf 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,37 @@ transactional-update NEWS -- history of user-visible changes. Copyright (C) 2016-2021 Thorsten Kukuk, Ignaz Forster et al. +Version 4.0.0~rc2 +* Fix missing prompt in "shell" command [bsc#1196580] +* Add output of tukit commands to log file +* Fix compilation error with GCC12 [boo#1194876] +* Fixed (non-critical) security review comments [boo#1196149] +* Fixed selfupdate +* Code cleanup + +Version 4.0.0~rc1 +This release is API, but not ABI compatible with previous releases; +existing applications will have to be recompiled against this new +version. +Major features: +* Introduces a D-Bus service to access the libtukit API via the + org.opensuse.tukit.Transaction interface +* Introduces a C binding via libtukit.h. +Other changes: +* t-u: Rework --quiet handling to make sure no output is shown even in + error cases; this is necessary for automation, e.g. with Salt. + [gh#openSUSE/transactional-update#73] +* tukit: Allow storing command output into variable by introducing a new + optional parameter for "execute" and "callExt". +* Replace multiple and non-standalone occurenses of {} in "callExt" + argument. +* Split transactional-update.timer into transactional-update.timer + and transactional-update-cleanup.timer; the later will clean up + old snapshots even when the system does not do automatic updates. +* tukit: Remove legacy alias "setDiscard" for "setDiscardIfUnchanged". +* Throw exception if snapshot is not found. +* Fix various compiler warnings + Version 3.6.2 * Bind mount root file system snapshot on itself, this makes the temporary directory in /tmp unnecessary; also fixes [boo#1188110] diff --git a/configure.ac b/configure.ac index 49299de..173537e 100644 --- a/configure.ac +++ b/configure.ac @@ -1,11 +1,11 @@ dnl Process this file with autoconf to produce a configure script. -AC_INIT(transactional-update, 3.6.2) +AC_INIT(transactional-update, 4.0.0~rc2) # Increase on any interface change and reset revision -LIBTOOL_CURRENT=3 +LIBTOOL_CURRENT=4 # Increase or reset on any VERSION update -LIBTOOL_REVISION=7 +LIBTOOL_REVISION=0 # Increase if interface change is backwards compatible, reset otherwise -LIBTOOL_AGE=3 +LIBTOOL_AGE=0 AC_CANONICAL_SYSTEM AM_INIT_AUTOMAKE([foreign]) AC_CONFIG_FILES([tukit.pc]) @@ -25,15 +25,23 @@ PKG_CHECK_VAR([DRACUTDIR], [dracut], [dracutmodulesdir], [], [AC_MSG_ERROR([Could not determine value for 'dracutmodulesdir' - is the 'dracut.pc' file installed?])]) PKG_CHECK_VAR([UDEVDIR], [udev], [udevdir], [], [AC_MSG_ERROR([Could not determine value for 'udevdir' - is the 'udev.pc' file installed?])]) +PKG_CHECK_VAR([DBUSDATADIR], [dbus-1], [datadir], [], + [AC_MSG_ERROR([Could not determine value for 'datadir' - is the 'dbus-1.pc' file installed?])]) +PKG_CHECK_VAR([DBUSSYSTEMBUSSERVICEDIR], [dbus-1], [system_bus_services_dir], [], + [AC_MSG_ERROR([Could not determine value for 'system_bus_services_dir' - is the 'dbus-1.pc' file installed?])]) UDEVRULESDIR=${UDEVDIR}/rules.d LOGROTATEDDIR=${sysconfdir}/logrotate.d +DBUSCONFDIR=${DBUSDATADIR}/dbus-1/system.d AC_SUBST(TMPFILESDIR) AC_SUBST(UDEVRULESDIR) AC_SUBST(SYSTEMDDIR) +AC_SUBST(SYSTEMDSYSTEMCONFDIR) AC_SUBST(DRACUTDIR) AC_SUBST(LOGROTATEDDIR) +AC_SUBST(DBUSCONFDIR) +AC_SUBST(DBUSSYSTEMBUSSERVICEDIR) AC_PROG_CXX AX_CXX_COMPILE_STDCXX_17(, mandatory) @@ -47,11 +55,20 @@ PKG_CHECK_MODULES([SELINUX], [libselinux]) PKG_CHECK_MODULES([LIBMOUNT], [mount]) PKG_CHECK_MODULES([LIBRPM], [rpm >= 4.15], AC_DEFINE([HAVE_RPMDBCOOKIE]), [PKG_CHECK_MODULES([LIBRPM], [rpm])]) +PKG_CHECK_MODULES([LIBSYSTEMD], [libsystemd]) AC_ARG_WITH([doc], [AS_HELP_STRING([--with-doc], [Build documentation])], , [enable_man=yes]) +dnl +dnl Checking for pthread support +dnl +m4_ifdef([AX_PTHREAD], + [AX_PTHREAD], + [AC_MSG_ERROR([Missing macro AX_PTHREAD: please install + autoconf-archive to enable pthreads checks.])]) + dnl dnl Check for xsltproc dnl @@ -65,6 +82,17 @@ JH_CHECK_XML_CATALOG([-//OASIS//DTD DocBook XML V4.3//EN], [DocBook XML DTD V4.3], [], enable_man=no) JH_CHECK_XML_CATALOG([http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl], [DocBook XSL Stylesheets], [], enable_man=no) +AC_PATH_PROG([BROWSER], [w3m]) +if test ! -z "$BROWSER"; then + BROWSER="$BROWSER -T text/html -dump" +else + AC_PATH_PROG([BROWSER], [elinks]) + if test ! -z "$BROWSER"; then + BROWSER="$BROWSER -no-numbering -no-references -dump" + else + enable_man=no + fi +fi m4_ifdef([AX_PYTHON_MODULE], [AX_PYTHON_MODULE([lxml])], @@ -80,16 +108,6 @@ AS_IF([test "x$enable_man" = "xno" -a "x$with_doc" != "xno"], AM_CONDITIONAL(ENABLE_REGENERATE_MAN, test "x$enable_man" != "xno" -a "x$with_doc" != "xno") -AC_PATH_PROG([BROWSER], [w3m]) -if test ! -z "$BROWSER"; then - BROWSER="$BROWSER -T text/html -dump" -else - AC_PATH_PROG([BROWSER], [links]) - if test ! -z "$BROWSER"; then - BROWSER="$BROWSER -no-numbering -no-references -dump" - fi -fi - AC_OUTPUT([Makefile lib/Makefile tukit/Makefile sbin/Makefile man/Makefile \ systemd/Makefile logrotate/Makefile dracut/Makefile doc/Makefile \ - etc/Makefile sbin/transactional-update]) + etc/Makefile dbus/Makefile sbin/transactional-update]) diff --git a/dbus/Makefile.am b/dbus/Makefile.am new file mode 100644 index 0000000..63fcde6 --- /dev/null +++ b/dbus/Makefile.am @@ -0,0 +1,16 @@ +# +# Copyright (c) 2020 Ignaz Forster +# + +sbin_PROGRAMS = tukitd +tukitd_SOURCES = tukitd.c +tukitd_CPPFLAGS = -I $(top_srcdir)/lib $(LIBSYSTEMD_CFLAGS) $(PTHREAD_CFLAGS) +tukitd_LDFLAGS = $(top_builddir)/lib/libtukit.la $(LIBSYSTEMD_LIBS) $(PTHREAD_CFLAGS) $(PTHREAD_LIBS) +dbusconfdir = @DBUSCONFDIR@ +dbusconf_DATA = org.opensuse.tukit.conf +dbussystembusservicedir = @DBUSSYSTEMBUSSERVICEDIR@ +dbussystembusservice_DATA = tukitd.d-bus.service +systemdsystemunitdir = @SYSTEMDDIR@ +systemdsystemunit_DATA = tukitd.service + +EXTRA_DIST = $(DATA) diff --git a/dbus/README.md b/dbus/README.md new file mode 100644 index 0000000..8876372 --- /dev/null +++ b/dbus/README.md @@ -0,0 +1,114 @@ +# Tukit DBUS Service +The tukitd service provides an DBUS interface which supports the same functionality as +the command line interface "tukit". + +## Starting/Stopping Servive +### Starting +The service will be started automatically with the first DBUS call, e.g. +> busctl introspect org.opensuse.tukit /org/opensuse/tukit/Transaction + +The status can also be checked via +> systemctl start tukitd.service +### Stopping +This `systemctl` call stops the service: +> systemctl stop tukitd.service + +## DBUS API +The following sections describe each call which is available via DBUS. +The command line program `busctl` can be used for demonstrating the API calls +and showing the results. + +### Transaction + +#### open +Creates a new transaction and returns its unique ID. + +Parameter: +* base - Snapshot ID (string), "active" or "default". Base can be "active" to base the + snapshot on the currently running system, "default" to the current default snapshot as a base + (which may or may not be identical to "active") or any specific existing snapshot id. + If base is not set (emtpy string) "active" will be used as the default. + +Return value: +* unique ID (string) + +Signal: +* TransactionOpened + +`busctl` example: + +> busctl call org.opensuse.tukit /org/opensuse/tukit/Transaction org.opensuse.tukit.Transaction open "s" "default" + +### call +Executes the given command from within the transaction's **chroot environment**, resuming the +transaction with the given ID; returns the exit status and the result of the given command. +In case of errors the snapshot will not be deleted. + +Parameter: +* unique ID (string) +* command (string) + +Return value: +None + +Signal: +* CommandExecuted - As this may be a long running operation, the results of the command are + returned via signal only. + +`busctl` example: + +* call `ls` in open transaction with ID `536`: + > busctl call org.opensuse.tukit /org/opensuse/tukit/Transaction org.opensuse.tukit.Transaction call "ss" "536" "bash -c 'ls'" + + The returned signal can be monitored by: + > busctl --system --match "path\_namespace='/org/opensuse/tukit'" monitor + +### callext +Executes the given command. The command is **not** executed in a **chroot environment**, but instead runs +in the current system, replacing '{}' with the mount directory of the given snapshot. +In case of errors the snapshot will not be deleted. + +Parameter: +* unique ID (string) +* command (string) + +Return value: +None + +Signal: +* CommandExecuted - As this may be a long running operation, the results of the command are + returned via signal only. + +`busctl` example: + +* copy file from active system into transaction with ID `536`: + > busctl call org.opensuse.tukit /org/opensuse/tukit/Transaction org.opensuse.tukit.Transaction callext "ss" "536" "bash -c 'mv /tmp/mylib {}/usr/lib'" + + The returned signal can be monitored by: + > busctl --system --match "path\_namespace='/org/opensuse/tukit'" monitor + +### close +Closes the given transaction and sets the snapshot as the new default snapshot. + +Parameter: +* unique ID (string) + +Return value: +* return integer; 0 on success + +`busctl` Example: + +> busctl call org.opensuse.tukit /org/opensuse/tukit/Transaction org.opensuse.tukit.Transaction close "s" "420" + +### abort +Deletes the given snapshot. + +Parameter: +* unique ID (string) + +Return value: +* return integer; 0 on success + +`busctl` Example: + +> busctl call org.opensuse.tukit /org/opensuse/tukit/Transaction org.opensuse.tukit.Transaction abort "s" "420" diff --git a/dbus/org.opensuse.tukit.conf b/dbus/org.opensuse.tukit.conf new file mode 100644 index 0000000..5aee7e5 --- /dev/null +++ b/dbus/org.opensuse.tukit.conf @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/dbus/tukitd.c b/dbus/tukitd.c new file mode 100644 index 0000000..ffe8397 --- /dev/null +++ b/dbus/tukitd.c @@ -0,0 +1,485 @@ +#include "Bindings/libtukit.h" +#include +#include +#include +#include +#include +#include +#include +#include + +enum transactionstates { queued, running, finished }; + +typedef struct t_entry { + char* id; + struct t_entry *next; + enum transactionstates state; +} TransactionEntry; + +// Even though userdata / activeTransaction is shared between several threads, due to +// systemd's serial event loop processing it's always guaranteed that no parallel +// access will be happen: lockSnapshot will be called in the event functions before +// starting the new thread, and unlockSnapshot is triggered by the signal when a +// thread has finished. +// Any method which does write the variable must do so from the main event loop. +int lockSnapshot(void* userdata, const char* transaction, sd_bus_error *ret_error) { + fprintf(stdout, "Locking further invocations for snapshot %s...\n", transaction); + TransactionEntry* activeTransaction = userdata; + TransactionEntry* newTransaction; + while (activeTransaction->id != NULL) { + if (strcmp(activeTransaction->id, transaction) == 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "The transaction is currently in use by another thread."); + return -EBUSY; + } + activeTransaction = activeTransaction->next; + } + if ((newTransaction = malloc(sizeof(TransactionEntry))) == NULL) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Error while allocating space for transaction."); + return -ENOMEM; + } + newTransaction->id = NULL; + activeTransaction->id = strdup(transaction); + if (activeTransaction->id == NULL) { + free(newTransaction); + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Error during strdup."); + return -ENOMEM; + } + activeTransaction->state = queued; + activeTransaction->next = newTransaction; + return 0; +} + +void unlockSnapshot(void* userdata, const char* transaction) { + TransactionEntry* activeTransaction = userdata; + TransactionEntry* prevNext = NULL; + + while (activeTransaction->id != NULL) { + // The entry point can't be changed, so just set the data of the to be deleted + // transaction to the next entry's data (also for later matches to have just one code + // path). + if (strcmp(activeTransaction->id, transaction) == 0) { + fprintf(stdout, "Unlocking snapshot %s...\n", transaction); + free(activeTransaction->id); + activeTransaction->id = activeTransaction->next->id; + prevNext = activeTransaction->next; + if (activeTransaction->id != NULL) { + activeTransaction->next = activeTransaction->next->next; + } + free(prevNext); + return; + } + activeTransaction = activeTransaction->next; + } +} + +sd_bus* get_bus() { + sd_bus *bus = NULL; + if (sd_bus_default_system(&bus) < 0) { + // When opening a new bus connection fails there aren't a lot of options to present this + // error to the user here. Maybe add some synchronization with the main thread and exit + // on error in the future? + fprintf(stderr, "Failed to connect to system bus."); + } + return bus; +} + +int send_error_signal(sd_bus *bus, const char *transaction, const char *message, int error) { + if (bus == NULL) { + bus = get_bus(); + } + int ret = sd_bus_emit_signal(bus, "/org/opensuse/tukit", "org.opensuse.tukit", "Error", "ssi", transaction, message, error); + if (ret < 0) { + // Something is seriously broken when even an error message can't be sent any more... + fprintf(stderr, "Cannot reach D-Bus any more: %s\n", strerror(ret)); + } + sd_bus_flush_close_unref(bus); + return ret; +} + +static int transaction_open(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + char *base; + const char *snapid; + int ret = 0; + + if (sd_bus_message_read(m, "s", &base) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Could not read base snapshot identifier."); + return -1; + } + struct tukit_tx* tx = tukit_new_tx(); + if (tx == NULL) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + return -1; + } + if ((ret = tukit_tx_init(tx, base)) != 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + } + if (!ret) { + snapid = tukit_tx_get_snapshot(tx); + if (snapid == NULL) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + ret = -1; + } + } + if (!ret && (ret = tukit_tx_keep(tx)) != 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + } + + tukit_free_tx(tx); + if (ret) { + return ret; + } + + if (sd_bus_emit_signal(sd_bus_message_get_bus(m), "/org/opensuse/tukit", "org.opensuse.tukit.Transaction", "TransactionOpened", "s", snapid) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Sending signal 'TransactionOpened' failed."); + return -1; + } + + fprintf(stdout, "Snapshot %s created.\n", snapid); + + ret = sd_bus_reply_method_return(m, "s", snapid); + free((void*)snapid); + return ret; +} + +struct execution_args { + char *transaction; + char *command; + int chrooted; + enum transactionstates *state; +}; + +static void *execution_func(void *args) { + int ret = 0; + int exec_ret = 0; + wordexp_t p; + + struct execution_args* ea = (struct execution_args*)args; + char transaction[strlen(ea->transaction) + 1]; // SIGSEGV + strcpy(transaction, ea->transaction); + char command[strlen(ea->command) + 1]; + strcpy(command, ea->command); + int chrooted = ea->chrooted; + + enum transactionstates *state = ea->state; + *state = running; + + fprintf(stdout, "Executing command `%s` in snapshot %s...\n", command, transaction); + + // The bus connection has been allocated in a parent process and is being now reused in the + // child process, so a new dbus connection has to be established (D-Bus doesn't support + // connection sharing between several threads). The bus will only be initialized directly + // before it is used to avoid timeouts. + sd_bus *bus = NULL; + + struct tukit_tx* tx = tukit_new_tx(); + if (tx == NULL) { + send_error_signal(bus, transaction, tukit_get_errmsg(), -1); + goto finish_execute; + } + ret = tukit_tx_resume(tx, transaction); + if (ret != 0) { + send_error_signal(bus, transaction, tukit_get_errmsg(), ret); + goto finish_execute; + } + + ret = wordexp(command, &p, 0); + if (ret != 0) { + if (ret == WRDE_NOSPACE) { + wordfree(&p); + } + send_error_signal(bus, transaction, "Command could not be processed.", ret); + goto finish_execute; + } + + const char* output; + if (chrooted) { + exec_ret = tukit_tx_execute(tx, p.we_wordv, &output); + } else { + exec_ret = tukit_tx_call_ext(tx, p.we_wordv, &output); + } + + wordfree(&p); + + ret = tukit_tx_keep(tx); + if (ret != 0) { + free((void*)output); + send_error_signal(bus, transaction, tukit_get_errmsg(), -1); + goto finish_execute; + } + + bus = get_bus(); + ret = sd_bus_emit_signal(bus, "/org/opensuse/tukit", "org.opensuse.tukit.Transaction", "CommandExecuted", "sis", transaction, exec_ret, output); + if (ret < 0) { + send_error_signal(bus, transaction, "Cannot send signal 'CommandExecuted'.", ret); + } + + free((void*)output); + +finish_execute: + sd_bus_flush_close_unref(bus); + tukit_free_tx(tx); + + return (void*)(intptr_t) ret; +} + +static int execute(sd_bus_message *m, void *userdata, + sd_bus_error *ret_error, const int chrooted) { + int ret; + pthread_t execute_thread; + struct execution_args exec_args; + TransactionEntry* activeTransaction = userdata; + + if (sd_bus_message_read(m, "ss", &exec_args.transaction, &exec_args.command) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Could not read D-Bus parameters."); + return -1; + } + + ret = lockSnapshot(userdata, exec_args.transaction, ret_error); + if (ret != 0) { + return ret; + } + + while (activeTransaction->next->id != NULL) { + activeTransaction = activeTransaction->next; + } + exec_args.chrooted = chrooted; + exec_args.state = &activeTransaction->state; + + ret = pthread_create(&execute_thread, NULL, execution_func, &exec_args); + while (activeTransaction->state != running) { + usleep(500); + } + + pthread_detach(execute_thread); + + return ret; +} + +static int transaction_call(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + int ret = execute(m, userdata, ret_error, 1); + if (ret) + return ret; + return sd_bus_reply_method_return(m, ""); +} + +static int transaction_callext(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + int ret = execute(m, userdata, ret_error, 0); + if (ret) + return ret; + return sd_bus_reply_method_return(m, ""); +} + +static int signalCallback(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + char *transaction; + + if (sd_bus_message_read(m, "s", &transaction) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Could not read transaction ID."); + return -1; + } + + unlockSnapshot(userdata, transaction); + return 0; +} + +static int transaction_close(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + char *transaction; + int ret = 0; + + if (sd_bus_message_read(m, "s", &transaction) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Could not read D-Bus parameters."); + return -1; + } + ret = lockSnapshot(userdata, transaction, ret_error); + if (ret != 0) { + return ret; + } + struct tukit_tx* tx = tukit_new_tx(); + if (tx == NULL) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + goto finish_close; + } + if ((ret = tukit_tx_resume(tx, transaction)) != 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + goto finish_close; + } + if ((ret = tukit_tx_finalize(tx)) != 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + goto finish_close; + } + + fprintf(stdout, "Snapshot %s closed.\n", transaction); + sd_bus_reply_method_return(m, "i", ret); + +finish_close: + tukit_free_tx(tx); + unlockSnapshot(userdata, transaction); + + return ret; +} + +static int transaction_abort(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) { + char *transaction; + int ret = 0; + + if (sd_bus_message_read(m, "s", &transaction) < 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", "Could not read D-Bus parameters."); + return -1; + } + ret = lockSnapshot(userdata, transaction, ret_error); + if (ret != 0) { + return ret; + } + struct tukit_tx* tx = tukit_new_tx(); + if (tx == NULL) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + goto finish_abort; + } + if ((ret = tukit_tx_resume(tx, transaction)) != 0) { + sd_bus_error_set_const(ret_error, "org.opensuse.tukit.Error", tukit_get_errmsg()); + goto finish_abort; + } + fprintf(stdout, "Snapshot %s aborted.\n", transaction); + sd_bus_reply_method_return(m, "i", ret); + +finish_abort: + tukit_free_tx(tx); + unlockSnapshot(userdata, transaction); + + return ret; +} + +int event_handler(sd_event_source *s, const struct signalfd_siginfo *si, void *userdata) { + TransactionEntry* activeTransaction = userdata; + if (activeTransaction->id != NULL) { + fprintf(stdout, "Waiting for remaining transactions to finish...\n"); + sleep(1); + kill(si->ssi_pid, si->ssi_signo); + // TODO: New requests should probably be rejected from here, but unlocking is an event itself... + } else { + fprintf(stdout, "Terminating.\n"); + int ret; + if ((ret = sd_event_exit(sd_event_source_get_event(s), 0)) < 0) { + fprintf(stderr, "Cannot exit the main loop! %s\n", strerror(-ret)); + exit(1); + } + } + return 0; +} + +static const sd_bus_vtable tukit_transaction_vtable[] = { + SD_BUS_VTABLE_START(0), + SD_BUS_METHOD_WITH_ARGS("Open", SD_BUS_ARGS("s", base), SD_BUS_RESULT("s", snapshot), transaction_open, 0), + SD_BUS_METHOD_WITH_ARGS("Call", SD_BUS_ARGS("s", transaction, "s", command), SD_BUS_NO_RESULT, transaction_call, 0), + SD_BUS_METHOD_WITH_ARGS("CallExt", SD_BUS_ARGS("s", transaction, "s", command), SD_BUS_NO_RESULT, transaction_callext, 0), + SD_BUS_METHOD_WITH_ARGS("Close", SD_BUS_ARGS("s", transaction), SD_BUS_RESULT("i", ret), transaction_close, 0), + SD_BUS_METHOD_WITH_ARGS("Abort", SD_BUS_ARGS("s", transaction), SD_BUS_RESULT("i", ret), transaction_abort, 0), + SD_BUS_SIGNAL_WITH_ARGS("TransactionOpened", SD_BUS_ARGS("s", snapshot), 0), + SD_BUS_SIGNAL_WITH_ARGS("CommandExecuted", SD_BUS_ARGS("s", snapshot, "i", returncode, "s", output), 0), + SD_BUS_VTABLE_END +}; + +int main() { + fprintf(stdout, "Started tukitd %s\n", VERSION); + + sd_bus_slot *slot = NULL; + sd_bus *bus = NULL; + sd_event *event = NULL; + int ret = 1; + + TransactionEntry* activeTransactions = (TransactionEntry*) malloc(sizeof(TransactionEntry)); + if (activeTransactions == NULL) { + fprintf(stderr, "malloc failed for TransactionEntry.\n"); + goto finish; + } + activeTransactions->id = NULL; + activeTransactions->state = queued; + + ret = sd_bus_open_system(&bus); + if (ret < 0) { + fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-ret)); + goto finish; + } + + ret = sd_bus_add_object_vtable(bus, + &slot, + "/org/opensuse/tukit/Transaction", + "org.opensuse.tukit.Transaction", + tukit_transaction_vtable, + activeTransactions); + if (ret < 0) { + fprintf(stderr, "Failed to issue method call: %s\n", strerror(-ret)); + goto finish; + } + + /* Take a well-known service name so that clients can find us */ + ret = sd_bus_request_name(bus, "org.opensuse.tukit", 0); + if (ret < 0) { + fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-ret)); + goto finish; + } + + ret = sd_bus_match_signal(bus, + NULL, + NULL, + "/org/opensuse/tukit", + NULL, + NULL, + signalCallback, + activeTransactions); + if (ret < 0) { + fprintf(stderr, "Failed to register DBus signal listener: %s\n", strerror(-ret)); + goto finish; + } + + ret = sd_event_default(&event); + if (ret < 0) { + fprintf(stderr, "Failed to create default event loop: %s\n", strerror(-ret)); + goto finish; + } + sigset_t ss; + if (sigemptyset(&ss) < 0 || sigaddset(&ss, SIGTERM) < 0 || sigaddset(&ss, SIGINT) < 0) { + fprintf(stderr, "Failed to set the signal set: %s\n", strerror(-ret)); + goto finish; + } + /* Block SIGTERM first, so that the event loop can handle it */ + if (sigprocmask(SIG_BLOCK, &ss, NULL) < 0) { + fprintf(stderr, "Failed to block the signals: %s\n", strerror(-ret)); + goto finish; + } + /* Let's make use of the default handler and "floating" reference features of sd_event_add_signal() */ + ret = sd_event_add_signal(event, NULL, SIGTERM, event_handler, activeTransactions); + if (ret < 0) { + fprintf(stderr, "Could not add signal handler for SIGTERM to event loop: %s\n", strerror(-ret)); + goto finish; + } + ret = sd_event_add_signal(event, NULL, SIGINT, event_handler, activeTransactions); + if (ret < 0) { + fprintf(stderr, "Could not add signal handler for SIGINT to event loop: %s\n", strerror(-ret)); + goto finish; + } + ret = sd_bus_attach_event(bus, event, 0); + if (ret < 0) { + fprintf(stderr, "Could not add sd-bus handling to event bus: %s\n", strerror(-ret)); + goto finish; + } + + ret = sd_event_loop(event); + if (ret < 0) { + fprintf(stderr, "Error while running event loop: %s\n", strerror(-ret)); + goto finish; + } + +finish: + while (activeTransactions && activeTransactions->id != NULL) { + TransactionEntry* nextTransaction = activeTransactions->next; + free(activeTransactions->id); + free(activeTransactions); + activeTransactions = nextTransaction; + } + free(activeTransactions); + sd_event_unref(event); + sd_bus_slot_unref(slot); + sd_bus_unref(bus); + + return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} diff --git a/dbus/tukitd.d-bus.service b/dbus/tukitd.d-bus.service new file mode 100644 index 0000000..d946c61 --- /dev/null +++ b/dbus/tukitd.d-bus.service @@ -0,0 +1,5 @@ +[D-BUS Service] +Name=org.opensuse.tukit +Exec=/usr/sbin/tukitd +User=root +SystemdService=tukitd.service \ No newline at end of file diff --git a/dbus/tukitd.service b/dbus/tukitd.service new file mode 100644 index 0000000..35d118c --- /dev/null +++ b/dbus/tukitd.service @@ -0,0 +1,7 @@ +[Unit] +Description=tukitd deamon for transactional update + +[Service] +Type=dbus +BusName=org.opensuse.tukit +ExecStart=/usr/sbin/tukitd \ No newline at end of file diff --git a/doc/png/Workflow-After-Reboot.png b/doc/png/Workflow-After-Reboot.png index bf9b130..82462ae 100644 Binary files a/doc/png/Workflow-After-Reboot.png and b/doc/png/Workflow-After-Reboot.png differ diff --git a/doc/png/Workflow-Start.png b/doc/png/Workflow-Start.png index 4464338..16f9179 100644 Binary files a/doc/png/Workflow-Start.png and b/doc/png/Workflow-Start.png differ diff --git a/doc/png/Workflow-Step1.png b/doc/png/Workflow-Step1.png deleted file mode 100644 index a2847d6..0000000 Binary files a/doc/png/Workflow-Step1.png and /dev/null differ diff --git a/doc/png/Workflow-Step2.png b/doc/png/Workflow-Step2.png index e528467..49514c8 100644 Binary files a/doc/png/Workflow-Step2.png and b/doc/png/Workflow-Step2.png differ diff --git a/doc/png/Workflow-Step3.png b/doc/png/Workflow-Step3.png index 0b7f745..315dd94 100644 Binary files a/doc/png/Workflow-Step3.png and b/doc/png/Workflow-Step3.png differ diff --git a/doc/png/Workflow-Step4.png b/doc/png/Workflow-Step4.png index 8bd7eb3..44fd1c9 100644 Binary files a/doc/png/Workflow-Step4.png and b/doc/png/Workflow-Step4.png differ diff --git a/doc/png/Workflow-Step5.png b/doc/png/Workflow-Step5.png index b96873a..3928600 100644 Binary files a/doc/png/Workflow-Step5.png and b/doc/png/Workflow-Step5.png differ diff --git a/doc/png/Workflow-Without-Reboot.png b/doc/png/Workflow-Without-Reboot.png index 0fc3b95..d941b19 100644 Binary files a/doc/png/Workflow-Without-Reboot.png and b/doc/png/Workflow-Without-Reboot.png differ diff --git a/doc/transactional-update.xml b/doc/transactional-update.xml index ed4bab1..a819527 100644 --- a/doc/transactional-update.xml +++ b/doc/transactional-update.xml @@ -16,7 +16,7 @@ iforster@suse.com - Version 0.3, 12. September 2019 + Version 0.4, 28. September 2021 This is the documentation for transactional-update and is intended for @@ -61,14 +61,17 @@ to continue using existing packages and tool chains for delivery and application of updates. While currently only implemented for (open)SUSE environments the concept is vendor independent and may also be - implemented for other package managers and package formats. + implemented for other package managers, package formats and file + systems. It consists of the (open)SUSE specific + transactional-update script and the + generic tukit library. - Conceptually transactional-update + Conceptually transactional-update creates a new snapshot with btrfs before performing any update and uses that snapshot for modifications. Since btrfs snapshots contain only the difference between two versions - and thus are usually very small updates done with + and thus are usually very small, updates done with transactional-update are very space efficient. This also means several snapshots can be installed at the same time @@ -120,15 +123,14 @@
- Motivation + Use Cases - Linux distributions have had working update mechanisms for many, many - years - so why do we need something new? Distributions evolved, - introducing new concepts such as rolling releases, containers or long + As Linux distributions are evolving over the years, new concepts are + appearing such rolling releases, containers, embedded systems or long time support releases. While the classical update mechanisms are - probably perfectly fine for a regular desktop user, using a - distribution with regular releases, other concepts may require - different concepts. + probably perfectly fine for a regular desktop user or a conventional + server system, the following example use cases may give an indication + why an even more error-proof system may be desirable: Distributions with rolling updates face @@ -143,9 +145,10 @@ working state. - On mission critical systems you want to - make sure that no service or user behaviour interferes with the update - of the system. And conversely the update should not modify the system, + On mission critical systems or + embedded systems one will usually want + to make sure that no service or user behaviour interferes with the + update of the system. Moreover the update should not modify the system, e.g. by uncontrolled restarts of services or unexpected modifications to the system in post scripts. Potential interruptions are deferred to a defined maintenance window instead. For really critical systems the @@ -164,58 +167,117 @@ the updates cluster aware. - Sometimes new kernel versions or software updates are - incompatible with your hardware or other software. In this case there - should be a quick and easy way to roll back to the state before the - update was applied. + To summarize: The update should only be applied if there were no + errors during the update. If it turns out that the update is + causing errors (e.g. because of a new kernel version incompatible + with the hardware) there should be a quick and easy way to roll back + to the state before the update was applied. +
+ + + + Components + + transactional-update is split into two parts: the (open)SUSE specific + transactional-update shell script, and the + generic tukit (including libtukit, the tukit + command line application and the D-Bus bindings). + +
+ libtukit.so - There are other solutions available for the above problems, like - downloading all RPMs upfront and apply them during the boot phase. - This however will block the system for an unknown period of time - while the update is running, delaying the availablility of the system. + libtukit is a C++ library implementing the core functionality of + transactional-update. It is responsible + for snapshot management, preparing the environment (including + overlay handling, see ) and executing + the command to run within the update environment. + + + The library is designed to be a general purpose library for handling + transactional systems. It provides methods to create, modify and close + transactions as well as execute commands within a transaction. + Currently snapper is the only implemented snapshot + management option. + + + Applications such as package managers are expected to use this library + for easily supporting transactional systems. DNF for example is + supporting transactional systems via the + libdnf-plugin-txnupd + plugin. + + + The library also provides C bindings with the same functionality as the + C++ library. + +
+
+ tukit + + tukit is a utility application to call libtukit + functionality from the command line. Applications which do not support + libtukit directly may use this application as + a wrapper. This command is not yet intended to be called by the user + directly, as it does not perform maintenance tasks such as marking a + snapshot for deletion for now. + +
+
+ D-Bus Bindings + + The libtukit functionality is also available via + D-Bus interface org.opensuse.tukit. Commands are + executed asynchronously, returning a signal when the command execution + is finished. + +
+
+ transactional-update + + This shell script is an (open)SUSE specific wrapper for handling the + tasks typical on a transactional system, e.g. installing packages, + updating the system or updating the bootloader. To do so it is using + the tukit wrapper to call applications such as + zypper for package management.
- Concept + High Level Concept
- Filesystem + The root file system - This chapter describes the handling of the root file system, i.e. the - core functionality of - transactional-update. Of course not - all information (such as - /var or - /home) should be stored on the - root volume, see for a real world setup. + This chapter describes the handling of the root file system. In general + transactional-update will not modify any + other subvolumes or file systems: Information stored on these mounts + (such as /var or + /home) is usually not supposed + to be rolled back. See for a real world + setup. + There are exceptions for a few dedicated subvolumes such as + /boot/grub2/x86_64-efi which + also have to be available to be able to update the bootloader; these + directories will be bind mounted into the update environment. transactional-update is based around several concepts of the Btrfs file system, a general purpose Copy-on-Write (Cow) filesystem with snapshot and - subvolume support. - Subvolumes look like a directory, but behave like a mount point. They - can be accessed from the parent subvolume like a directory, or they can - be mounted on other directories of the same filesytem. - Snapshots will be created from existing subvolumes, excluding other - subvolumes inside of it, and are read-only by default. - - - Implementation note: transactional-update - may also be implemented for any other file system as long as it provides - snapshot functionality and the ability to boot from snapshots. See - for requirements and porting information. + subvolume support, though support for other file systems is possible + (see for requirements and porting + information).
Updating the correct snapshot - transactional-update is using zypper with the - option pointing to the new snapshot for package - management. Other commands (such as the creation of initrd) will be - called with chroot. + transactional-update is wrapping all binaries which will modify the + file system with tukit, which will in turn use + chroot to execute the command in the new snapshot. + That way e.g. zypper will install packages + into the new snapshot only.
@@ -229,22 +291,9 @@ - At the beginning, there is a list of old snapshots, each one based - on the other one, and the newest one is the current root filesystem. - - - - - - - - - List of snapshots with new read-only Clone of current root filesystem - - - - In the first step, a new read-only snapshot of the current root - filesystem will be created. + In the beginning there is a list of old snapshots, each one based + on the previous one, and the newest one is the current root + file system. @@ -253,12 +302,12 @@ - List of snapshots with a read-write Clone of current root filesystem + List of snapshots with a read-write clone of the current root file system. - In the second step we switch the snapshot from read-only to - read-write, so that we can update it. + In the first step, a new snapshot of the current root + file system will be created. This snapshot is set read write. @@ -267,12 +316,12 @@ - List of snapshots with a read-write Clone of current root - filesystem, which will be updated with zypper. + List of snapshots with a read-write clone of current root + file system, which will be updated with zypper. - In the third step the snapshot will be updated. This can be + In the second step the snapshot will be updated. This can be zypper up or zypper dup, the installation or removal of a package or any other modification to the root file system. @@ -284,11 +333,11 @@ - List of snapshots with the clone again read-only. + List of snapshots with the clone set read-only. - In the fourth step the snapshot will be changed back to read-only, + In the third step the snapshot will be changed back to read-only, so that the data cannot be modified anymore. @@ -298,12 +347,12 @@ - List of snapshots with the read-only Clone the new default. + List of snapshots with the read-only clone set as the new default. The last step is to mark the updated snapshot as new root - filesystem. This is the atomic step: If the power would have + file system. This is the atomic step: If the power would have been pulled before, the unchanged old system would have been booted. Now the new, updated system will boot. @@ -314,13 +363,13 @@ - List of snapshots with the current root filesystem as newest + List of snapshots with the current root file system as newest at the end. After reboot, the newly prepared snapshot is the new root - filesystem. In case anything goes wrong a rollback to any of + file system. In case anything goes wrong a rollback to any of the older snapshots can be performed. @@ -330,7 +379,7 @@ - List of snapshots with a read-write Clone of current root + List of snapshots with a read-write clone of current root filesystem, which will be updated with zypper. @@ -342,7 +391,9 @@ not on the new default snapshot! For stacking changes (i.e. if several commands are supposed to be combined in one single snapshot) the command - can be used to perform any number of operations. + can be used to perform any number of operations, or the + can be used to continue the latest + snapshot while preserving a separate snapshot for each step. @@ -356,12 +407,7 @@ -SNAPSHOT_ID=`snapper create -p -d "Snapshot Update"` - - - - -btrfs property set ${SNAPSHOT_DIR} ro false +SNAPSHOT_ID=`snapper create --read-write -p -d "Snapshot Update"` @@ -405,8 +451,8 @@ systemctl reboot /var should not be part of the root file system, otherwise doing a rollback to a previous state would also roll back the /var - contents. On a read-only system this directory also has be mounted in - read-write mode anyway, as several variable data is written into it. + contents. On a read-only system this directory has to be writable in any + case, variable data is stored inside. Due to the volatile nature of @@ -428,8 +474,9 @@ systemctl reboot Packaging for transactional-updates and Migration / Upgrade - in the Packaging guidelines for more information. If a package is - breaking this rule a warning message indicating the affected file is + in the Packaging guidelines. If a package is breaking this rule by + installing files into a directory which is not part of the root file + system, then a warning message indicating the affected file is printed at the end of the transactional-update run. @@ -561,8 +608,10 @@ systemctl reboot Porting to other systems - You need a CoW filesystem (or anything else with snapshots - and rollback), else this should work with every package manager. + Currently snapper is the only supported snapshot + implementation, however it is prepared to support other (file) systems + as long as they provide snapshot functionality and the ability to boot + from specific snapshots. diff --git a/lib/Bindings/CBindings.cpp b/lib/Bindings/CBindings.cpp new file mode 100644 index 0000000..c070c9e --- /dev/null +++ b/lib/Bindings/CBindings.cpp @@ -0,0 +1,159 @@ +/* + SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: SUSE LLC */ + +#include "libtukit.h" +#include "Log.hpp" +#include "Transaction.hpp" +#include +#include +#include + +using namespace TransactionalUpdate; +thread_local std::string errmsg; + +const char* tukit_get_errmsg() { + return errmsg.c_str(); +} +void tukit_set_loglevel(loglevel lv) { + tulog.level = static_cast(lv); +} +tukit_tx tukit_new_tx() { + Transaction* transaction = nullptr; + try { + transaction = new Transaction; + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + } + return reinterpret_cast(transaction); +} +void tukit_free_tx(tukit_tx tx) { + if (tx != nullptr) { + delete reinterpret_cast(tx); + } +} +int tukit_tx_init(tukit_tx tx, char* base) { + Transaction* transaction = reinterpret_cast(tx); + try { + if (std::string(base).empty()) + transaction->init("active"); + else + transaction->init(base); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_discard_if_unchanged(tukit_tx tx, int discard) { + Transaction* transaction = reinterpret_cast(tx); + try { + transaction->setDiscardIfUnchanged(discard); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_resume(tukit_tx tx, char* id) { + Transaction* transaction = reinterpret_cast(tx); + try { + transaction->resume(id); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_execute(tukit_tx tx, char* argv[], const char* output[]) { + Transaction* transaction = reinterpret_cast(tx); + std::string buffer; + try { + int ret = transaction->execute(argv, &buffer); + if (output) { + *output = strdup(buffer.c_str()); + } + return ret; + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } +} +int tukit_tx_call_ext(tukit_tx tx, char* argv[], const char* output[]) { + Transaction* transaction = reinterpret_cast(tx); + std::string buffer; + try { + int ret = transaction->callExt(argv, &buffer); + if (output) { + *output = strdup(buffer.c_str()); + } + return ret; + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } +} +int tukit_tx_finalize(tukit_tx tx) { + Transaction* transaction = reinterpret_cast(tx); + try { + transaction->finalize(); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_keep(tukit_tx tx) { + Transaction* transaction = reinterpret_cast(tx); + try { + transaction->keep(); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_send_signal(tukit_tx tx, int signal) { + Transaction* transaction = reinterpret_cast(tx); + try { + transaction->sendSignal(signal); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return -1; + } + return 0; +} +int tukit_tx_is_initialized(tukit_tx tx) { + Transaction* transaction = reinterpret_cast(tx); + return transaction->isInitialized(); +} +/* Free return string with free() */ +const char* tukit_tx_get_snapshot(tukit_tx tx) { + Transaction* transaction = reinterpret_cast(tx); + try { + return strdup(transaction->getSnapshot().c_str()); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return nullptr; + } +} +const char* tukit_tx_get_root(tukit_tx tx) { + Transaction* transaction = reinterpret_cast(tx); + try { + return transaction->getRoot().c_str(); + } catch (const std::exception &e) { + fprintf(stderr, "ERROR: %s\n", e.what()); + errmsg = e.what(); + return nullptr; + } +} diff --git a/lib/Bindings/libtukit.h b/lib/Bindings/libtukit.h new file mode 100644 index 0000000..ead72ee --- /dev/null +++ b/lib/Bindings/libtukit.h @@ -0,0 +1,42 @@ +/* + SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2020 SUSE LLC */ + +/* + This is the EXPERIMENTAL C API for tukit. For the moment it is only inteded + for internal use. + For documentation please see the corresponding classes in the C++ header + files. + */ + +#ifndef T_U_TUKIT_H +#define T_U_TUKIT_H +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + None=0, Error, Info, Debug +} loglevel; + +const char* tukit_get_errmsg(); +void tukit_set_loglevel(loglevel lv); +typedef void* tukit_tx; +tukit_tx tukit_new_tx(); +void tukit_free_tx(tukit_tx tx); +int tukit_tx_init(tukit_tx tx, char* base); +int tukit_tx_discard_if_unchanged(tukit_tx tx, int discard); +int tukit_tx_resume(tukit_tx tx, char* id); +int tukit_tx_execute(tukit_tx tx, char* argv[], const char* output[]); +int tukit_tx_call_ext(tukit_tx tx, char* argv[], const char* output[]); +int tukit_tx_finalize(tukit_tx tx); +int tukit_tx_keep(tukit_tx tx); +int tukit_tx_send_signal(tukit_tx tx, int signal); +int tukit_tx_is_initialized(tukit_tx tx); +const char* tukit_tx_get_snapshot(tukit_tx tx); +const char* tukit_tx_get_root(tukit_tx tx); + +#ifdef __cplusplus +} +#endif +#endif // T_U_TUKIT_H diff --git a/lib/Makefile.am b/lib/Makefile.am index 6677133..bd61f98 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -1,12 +1,14 @@ AUTOMAKE_OPTIONS = subdir-objects lib_LTLIBRARIES = libtukit.la libtukit_la_SOURCES=Transaction.cpp \ - Snapshot.cpp Snapshot/Snapper.cpp \ + SnapshotManager.cpp Snapshot/Snapper.cpp \ Mount.cpp Overlay.cpp Configuration.cpp \ - Util.cpp Supplement.cpp + Util.cpp Supplement.cpp Bindings/CBindings.cpp publicheadersdir=$(includedir)/tukit -publicheaders_HEADERS=Transaction.hpp -noinst_HEADERS=Snapshot.hpp Snapshot/Snapper.hpp \ +publicheaders_HEADERS=Transaction.hpp \ + Snapshot.hpp SnapshotManager.hpp \ + Bindings/libtukit.h +noinst_HEADERS=Snapshot/Snapper.hpp \ Mount.hpp Overlay.hpp Log.hpp Configuration.hpp \ Util.hpp Supplement.hpp Exceptions.hpp libtukit_la_CPPFLAGS=-DPREFIX=\"$(prefix)\" -DCONFDIR=\"$(sysconfdir)\" $(ECONF_CFLAGS) $(LIBMOUNT_CFLAGS) $(SELINUX_CFLAGS) diff --git a/lib/Mount.cpp b/lib/Mount.cpp index 204f974..9174f05 100644 --- a/lib/Mount.cpp +++ b/lib/Mount.cpp @@ -101,8 +101,9 @@ void Mount::removeOption(std::string option) { throw std::runtime_error{"File system option " + option + "could not be removed: " + std::to_string(rc)}; } if ((rc = mnt_fs_set_options(mnt_fs, new_opts)) != 0) { + std::string snew_opts = std::string(new_opts); free(new_opts); - throw std::runtime_error{"Could not set new options " + std::string(new_opts) + " for file system " + mountpoint + ": " + std::to_string(rc)}; + throw std::runtime_error{"Could not set new options " + snew_opts + " for file system " + mountpoint + ": " + std::to_string(rc)}; } free(new_opts); } @@ -136,8 +137,9 @@ void Mount::setOption(std::string option, std::string value) { throw std::runtime_error{"File system option " + option + "could not be set to " + value + ": " + std::to_string(rc)}; } if ((rc = mnt_fs_set_options(mnt_fs, new_opts)) != 0) { + std::string snew_opts = std::string(new_opts); free(new_opts); - throw std::runtime_error{"Could not set new options " + std::string(new_opts) + " for file system " + mountpoint + ": " + std::to_string(rc)}; + throw std::runtime_error{"Could not set new options " + std::string(snew_opts) + " for file system " + mountpoint + ": " + std::to_string(rc)}; } free(new_opts); } @@ -260,7 +262,7 @@ void Mount::umountRecursive(libmnt_table* umount_table, libmnt_fs* umount_fs) { } int rc = mnt_context_umount(umount_cxt); char buf[BUFSIZ] = { 0 }; - rc = mnt_context_get_excode(umount_cxt, rc, buf, sizeof(buf)); + mnt_context_get_excode(umount_cxt, rc, buf, sizeof(buf)); if (*buf) tulog.error("Error unmounting '", mnt_fs_get_target(umount_fs), "': ", buf); } diff --git a/lib/Overlay.cpp b/lib/Overlay.cpp index 2454095..024668f 100644 --- a/lib/Overlay.cpp +++ b/lib/Overlay.cpp @@ -10,8 +10,8 @@ #include "Configuration.hpp" #include "Log.hpp" #include "Mount.hpp" -#include "Snapshot.hpp" #include "Util.hpp" +#include #include #include #include @@ -45,8 +45,8 @@ Overlay::Overlay(string snapshot): // Note: Due to this for new overlays (i.e. when "create" will be called later) the lowerdirs // will be initialized with outdated data of the base snapshot - it will be initalized // correctly during "create". - unique_ptr snap = SnapshotFactory::get(); - snap->open(snapshot); + snapMgr = SnapshotFactory::get(); + unique_ptr snap = snapMgr->open(snapshot); Mount mntEtc{"/etc"}; mntEtc.setTabSource(snap->getRoot() / "etc" / "fstab"); // Read data from fstab if this is an existing snapshot, just use the defaults otherwise @@ -97,9 +97,9 @@ void Overlay::sync(string base, fs::path snapRoot) { return; } - unique_ptr previousSnapshot = SnapshotFactory::get(); + unique_ptr previousSnapshot; try { - previousSnapshot->open(previousSnapId); + previousSnapshot = snapMgr->open(previousSnapId); } catch (std::invalid_argument &e) { tulog.info("Parent snapshot ", previousSnapId, " does not exist any more - skipping rsync"); return; @@ -169,8 +169,7 @@ void Overlay::setMountOptionsForMount(unique_ptr& mount) { } // Replace /etc in lowerdir with /etc of overlay base if (lowerdir == "/etc") { - std::unique_ptr snap = SnapshotFactory::get(); - snap->open(getIdOfOverlayDir(upperdir)); + std::unique_ptr snap = snapMgr->open(getIdOfOverlayDir(upperdir)); lower.append(snap->getRoot() / "etc"); } else { lower.append(lowerdir); diff --git a/lib/Overlay.hpp b/lib/Overlay.hpp index 12bc306..5594b54 100644 --- a/lib/Overlay.hpp +++ b/lib/Overlay.hpp @@ -9,6 +9,7 @@ #define T_U_OVERLAY_H #include "Mount.hpp" +#include "SnapshotManager.hpp" #include #include #include @@ -31,6 +32,7 @@ class Overlay { std::filesystem::path workdir; private: static std::string getIdOfOverlayDir(const std::string dir); + std::unique_ptr snapMgr; }; } // namespace TransactionalUpdate diff --git a/lib/Snapshot.hpp b/lib/Snapshot.hpp index 0b3e913..51ecd30 100644 --- a/lib/Snapshot.hpp +++ b/lib/Snapshot.hpp @@ -17,15 +17,11 @@ namespace TransactionalUpdate { class Snapshot { public: - Snapshot() = default; + Snapshot(std::string id): snapshotId{id} {}; virtual ~Snapshot() = default; - virtual void create(std::string base) = 0; - virtual void open(std::string id) = 0; virtual void close() = 0; virtual void abort() = 0; virtual std::filesystem::path getRoot() = 0; - virtual std::string getCurrent() = 0; - virtual std::string getDefault() = 0; virtual bool isInProgress() = 0; virtual bool isReadOnly() = 0; virtual void setDefault() = 0; @@ -35,11 +31,6 @@ class Snapshot { std::string snapshotId; }; -class SnapshotFactory { -public: - static std::unique_ptr get(); -}; - } // namespace TransactionalUpdate #endif // T_U_SNAPSHOT_H diff --git a/lib/Snapshot/Snapper.cpp b/lib/Snapshot/Snapper.cpp index 4633a49..f68d4be 100644 --- a/lib/Snapshot/Snapper.cpp +++ b/lib/Snapshot/Snapper.cpp @@ -13,15 +13,19 @@ namespace TransactionalUpdate { -void Snapper::create(std::string base) { +std::unique_ptr Snapper::create(std::string base) { + if (! std::filesystem::exists("/.snapshots/" + base + "/snapshot")) + throw std::invalid_argument{"Base snapshot '" + base + "' does not exist."}; snapshotId = callSnapper("create --from " + base + " --read-write --print-number --description 'Snapshot Update of #" + base + "' --userdata 'transactional-update-in-progress=yes'"); Util::rtrim(snapshotId); + return std::make_unique(snapshotId); } -void Snapper::open(std::string id) { +std::unique_ptr Snapper::open(std::string id) { snapshotId = id; if (! std::filesystem::exists(getRoot())) throw std::invalid_argument{"Snapshot " + id + " does not exist."}; + return std::make_unique(snapshotId); } void Snapper::close() { @@ -36,10 +40,6 @@ std::filesystem::path Snapper::getRoot() { return std::filesystem::path("/.snapshots/" + snapshotId + "/snapshot"); } -std::string Snapper::getUid() { - return snapshotId; -} - std::string Snapper::getCurrent() { std::string id = callSnapper("--csvout list --columns active,number"); std::smatch match; diff --git a/lib/Snapshot/Snapper.hpp b/lib/Snapshot/Snapper.hpp index af40f57..1da6cb0 100644 --- a/lib/Snapshot/Snapper.hpp +++ b/lib/Snapshot/Snapper.hpp @@ -8,32 +8,35 @@ #ifndef T_U_SNAPPER_H #define T_U_SNAPPER_H -#include "Snapshot.hpp" +#include "SnapshotManager.hpp" #include #include namespace TransactionalUpdate { -class Snapper: public Snapshot { +class Snapper: public SnapshotManager, public Snapshot { public: - Snapper() = default; ~Snapper() = default; - void create(std::string base); - void open(std::string id); - void close(); - void abort(); - std::filesystem::path getRoot(); - std::string getUid(); - std::string getCurrent(); - std::string getDefault(); - bool isInProgress(); - bool isReadOnly(); - void setDefault(); - void setReadOnly(bool readonly); + // Snapshot + Snapper(std::string snap): Snapshot(snap) {}; + void close() override; + void abort() override; + std::filesystem::path getRoot() override; + bool isInProgress() override; + bool isReadOnly() override; + void setDefault() override; + void setReadOnly(bool readonly) override; + + // SnapshotManager + Snapper(): Snapshot("") {}; + std::unique_ptr create(std::string base) override; + virtual std::unique_ptr open(std::string id) override; + std::string getCurrent() override; + std::string getDefault() override; private: std::string callSnapper(std::string); - bool snapperNoDbus = false; + inline static bool snapperNoDbus; }; } // namespace TransactionalUpdate diff --git a/lib/Snapshot.cpp b/lib/SnapshotManager.cpp similarity index 79% rename from lib/Snapshot.cpp rename to lib/SnapshotManager.cpp index 9462f9e..9fa7663 100644 --- a/lib/Snapshot.cpp +++ b/lib/SnapshotManager.cpp @@ -2,18 +2,17 @@ /* SPDX-FileCopyrightText: 2020 SUSE LLC */ /* - Factory / interface class for snapshot generation implementations; + Factory / interface class for snapshot management; implementations can be found in the "Snapshot" directory */ -#include "Snapshot.hpp" #include "Snapshot/Snapper.hpp" using namespace std; namespace TransactionalUpdate { // TODO: Make configurable to be able to force a certain implementation -unique_ptr SnapshotFactory::get() { +unique_ptr SnapshotFactory::get() { if (filesystem::exists("/usr/bin/snapper")) { return make_unique(); } else { diff --git a/lib/SnapshotManager.hpp b/lib/SnapshotManager.hpp new file mode 100644 index 0000000..460de50 --- /dev/null +++ b/lib/SnapshotManager.hpp @@ -0,0 +1,38 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ +/* SPDX-FileCopyrightText: 2021 SUSE LLC */ + +/* + The SnapshotManager class is the central API class for general snapshot + mangement, i.e. for everything which is not bound to a specific transaction + such as listing all snapshots or getting the default snapshot. + + Use the Transaction class for calling creating snapshots. + */ + +#ifndef T_U_SNAPSHOTMANAGER_H +#define T_U_SNAPSHOTMANAGER_H + +#include +#include + +namespace TransactionalUpdate { + +class SnapshotManager +{ +public: + SnapshotManager() = default; + virtual ~SnapshotManager() = default; + virtual std::unique_ptr create(std::string base) = 0; + virtual std::unique_ptr open(std::string id) = 0; + virtual std::string getCurrent() = 0; + virtual std::string getDefault() = 0; +}; + +class SnapshotFactory { +public: + static std::unique_ptr get(); +}; + +} // namespace TransactionalUpdate + +#endif // T_U_SNAPSHOTMANAGER_H diff --git a/lib/Transaction.cpp b/lib/Transaction.cpp index af03d92..412c1c1 100644 --- a/lib/Transaction.cpp +++ b/lib/Transaction.cpp @@ -12,7 +12,7 @@ #include "Log.hpp" #include "Mount.hpp" #include "Overlay.hpp" -#include "Snapshot.hpp" +#include "SnapshotManager.hpp" #include "Supplement.hpp" #include "Util.hpp" #include @@ -39,9 +39,10 @@ class Transaction::impl { public: void addSupplements(); void mount(); - int runCommand(char* argv[], bool inChroot); + int runCommand(char* argv[], bool inChroot, std::string* buffer); static int inotifyAdd(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb); int inotifyRead(); + std::unique_ptr snapshotMgr; std::unique_ptr snapshot; std::vector> dirsToMount; Supplements supplements; @@ -54,7 +55,7 @@ Transaction::Transaction() : pImpl{std::make_unique()} { if (getenv("TRANSACTIONAL_UPDATE") != NULL) { throw std::runtime_error{"Cannot open a new transaction from within a running transaction."}; } - pImpl->snapshot = SnapshotFactory::get(); + pImpl->snapshotMgr = SnapshotFactory::get(); } Transaction::~Transaction() { @@ -199,12 +200,12 @@ int Transaction::impl::inotifyAdd(const char *pathname, const struct stat *sbuf, return 0; } -void Transaction::init(std::string base = "active") { +void Transaction::init(std::string base) { if (base == "active") - base = pImpl->snapshot->getCurrent(); + base = pImpl->snapshotMgr->getCurrent(); else if (base == "default") - base = pImpl->snapshot->getDefault(); - pImpl->snapshot->create(base); + base = pImpl->snapshotMgr->getDefault(); + pImpl->snapshot = pImpl->snapshotMgr->create(base); tulog.info("Using snapshot " + base + " as base for new snapshot " + pImpl->snapshot->getUid() + "."); @@ -236,7 +237,7 @@ void Transaction::init(std::string base = "active") { } void Transaction::resume(std::string id) { - pImpl->snapshot->open(id); + pImpl->snapshot = pImpl->snapshotMgr->open(id); if (! pImpl->snapshot->isInProgress()) { pImpl->snapshot.reset(); throw std::invalid_argument{"Snapshot " + id + " is not an open transaction."}; @@ -251,9 +252,6 @@ void Transaction::resume(std::string id) { void Transaction::setDiscardIfUnchanged(bool discard) { pImpl->discardIfNoChange = discard; } -void Transaction::setDiscard(bool discard) { - setDiscardIfUnchanged(discard); -} int Transaction::impl::inotifyRead() { size_t bufLen = sizeof(struct inotify_event) + NAME_MAX + 1; @@ -276,7 +274,7 @@ int Transaction::impl::inotifyRead() { return ret; } -int Transaction::impl::runCommand(char* argv[], bool inChroot) { +int Transaction::impl::runCommand(char* argv[], bool inChroot, std::string* output) { if (discardIfNoChange) { inotifyFd = inotify_init(); if (inotifyFd == -1) @@ -300,10 +298,32 @@ int Transaction::impl::runCommand(char* argv[], bool inChroot) { int status = 1; int ret; + int pipefd[2]; + + ret = pipe(pipefd); + if (ret < 0) { + throw std::runtime_error{"Error opening pipe for command output: " + std::string(strerror(errno))}; + } + pid_t pid = fork(); if (pid < 0) { throw std::runtime_error{"fork() failed: " + std::string(strerror(errno))}; } else if (pid == 0) { + if (output != nullptr) { + ret = dup2(pipefd[1], STDOUT_FILENO); + if (ret < 0) { + throw std::runtime_error{"Redirecting stdout failed: " + std::string(strerror(errno))}; + } + ret = dup2(pipefd[1], STDERR_FILENO); + if (ret < 0) { + throw std::runtime_error{"Redirecting stderr failed: " + std::string(strerror(errno))}; + } + ret = close(pipefd[0]); + if (ret < 0) { + throw std::runtime_error{"Closing pipefd failed: " + std::string(strerror(errno))}; + } + } + if (inChroot) { if (chdir(snapshot->getRoot().c_str()) < 0) { tulog.info("Warning: Couldn't set working directory: ", std::string(strerror(errno))); @@ -312,6 +332,7 @@ int Transaction::impl::runCommand(char* argv[], bool inChroot) { throw std::runtime_error{"Chrooting to " + std::string(snapshot->getRoot()) + " failed: " + std::string(strerror(errno))}; } } + // Set indicator for RPM pre/post sections to detect whether we run in a // transactional update setenv("TRANSACTIONAL_UPDATE", "true", 1); @@ -320,6 +341,18 @@ int Transaction::impl::runCommand(char* argv[], bool inChroot) { } ret = -1; } else { + ret = close(pipefd[1]); + if (ret < 0) { + throw std::runtime_error{"Closing pipefd failed: " + std::string(strerror(errno))}; + } + if (output != nullptr) { + char buffer[2048]; + ssize_t len; + while((len = read(pipefd[0], buffer, 2048)) > 0) { + output->append(buffer, len); + } + } + this->pidCmd = pid; ret = waitpid(pid, &status, 0); this->pidCmd = 0; @@ -339,18 +372,22 @@ int Transaction::impl::runCommand(char* argv[], bool inChroot) { return ret; } -int Transaction::execute(char* argv[]) { - return this->pImpl->runCommand(argv, true); +int Transaction::execute(char* argv[], std::string* output) { + return this->pImpl->runCommand(argv, true, output); } -int Transaction::callExt(char* argv[]) { +int Transaction::callExt(char* argv[], std::string* output) { for (int i=0; argv[i] != nullptr; i++) { - if (strcmp(argv[i], "{}") == 0) { - char* bindDir = strdup(getRoot().c_str()); - argv[i] = bindDir; - } + std::string s = std::string(argv[i]); + std::string from = "{}"; + // replacing all {} by bindDir + for(size_t pos = 0; + (pos = s.find(from, pos)) != std::string::npos; + pos += getRoot().string().length()) + s.replace(pos, from.size(), getRoot()); + argv[i] = strdup(s.c_str()); } - return this->pImpl->runCommand(argv, false); + return this->pImpl->runCommand(argv, false, output); } void Transaction::sendSignal(int signal) { @@ -388,8 +425,7 @@ void Transaction::finalize() { pImpl->supplements.cleanup(); pImpl->dirsToMount.clear(); - std::unique_ptr defaultSnap = SnapshotFactory::get(); - defaultSnap->open(pImpl->snapshot->getDefault()); + std::unique_ptr defaultSnap = pImpl->snapshotMgr->open(pImpl->snapshotMgr->getDefault()); if (defaultSnap->isReadOnly()) pImpl->snapshot->setReadOnly(true); pImpl->snapshot->setDefault(); diff --git a/lib/Transaction.hpp b/lib/Transaction.hpp index 421d04b..501b99f 100644 --- a/lib/Transaction.hpp +++ b/lib/Transaction.hpp @@ -13,10 +13,7 @@ #ifndef T_U_TRANSACTION_H #define T_U_TRANSACTION_H -#include #include -#include -#include namespace TransactionalUpdate { @@ -69,8 +66,6 @@ class Transaction { * listeners already. Changes may not be detected correctly in this case. */ void setDiscardIfUnchanged(bool discard); - /** Temporary legacy name - remove on the next incompatible interface change. **/ - void setDiscard(bool discard); /** * @brief Resume an existing transaction @@ -86,13 +81,13 @@ class Transaction { * @return application's return code * * Execute any given command within the new snapshot. The application's output will be - * printed to the corresponding streams. + * printed to the corresponding streams or, if set, stored in the output variable. * * Note that @param is following the default C style syntax: * @example: char *args[] = {(char*)"ls", (char*)"-l", NULL}; int status = transaction.execute(args); */ - int execute(char* argv[]); + int execute(char* argv[], std::string *output=nullptr); /** * @brief Replace '{}' in argv with mount directory and execute command @@ -103,13 +98,14 @@ class Transaction { * and execute the given command *outside* of the snapshot in the running system. This may * be useful if the command needs access to the current environment, e.g. to copy a file * from a directory not accessible from within the chroot environment. - * The application's output will be printed to the corresponding streams. + * The application's output will be printed to the corresponding streams or, if set, + * stored in the output variable. * * Note that @param is following the default C style syntax: * @example: char *args[] = {(char*)"zypper", (char*)"-R", (char*)"{}", (char*)"up", NULL}; - int status = transaction.execute(args); + int status = transaction.callExt(args); */ - int callExt(char* argv[]); + int callExt(char* argv[], std::string *output=nullptr); /** * @brief Close a transaction and set it as the new default snapshot diff --git a/man/transactional-update.8.xml b/man/transactional-update.8.xml index d1ed455..537064e 100644 --- a/man/transactional-update.8.xml +++ b/man/transactional-update.8.xml @@ -543,7 +543,13 @@ for more information. - Don't print warnings and informational messages to stdout. + Don't print warnings and informational messages to stdout + when generated by transactional-update itself, i.e. when using the + command only the actual output of the command + will be shown, for other commands the + option will be appended to the corresponding helper applications if + available (e.g. to the zypper command for the + Package Commands). diff --git a/sbin/transactional-update.in b/sbin/transactional-update.in index b17ed0e..3516a95 100755 --- a/sbin/transactional-update.in +++ b/sbin/transactional-update.in @@ -64,6 +64,7 @@ SYSTEM_MANIFEST_FILE="@libdir@/sysimage/tu/system.manifest" ZYPPER_AUTO_IMPORT_KEYS=0 ETC_OVERLAY_PATTERN='^[^[:space:]]\+[[:space:]]\+\/etc[[:space:]]\+overlay[[:space:]]\+\([^[:space:]]*,\|\)workdir=\/sysroot\/var\/lib\/overlay\/work-etc[,[:space:]]' NON_ROOTFS_WHITELIST=("/var/lib/YaST2/cookies" "/var/lib/rpm" "/var/lib/systemd/migrated" "/var/run/zypp.pid") +DRACUT_OPTS="" TUKIT_OPTS="" TMPDIR=${TMPDIR:-/tmp} @@ -90,13 +91,6 @@ declare -A ROLES=() REPOS=() declare -A BUILDTIME=() -# Create stderr alias for things that shouldn't be logged into logfile -if [ ! -e /proc/$$/fd/4 ]; then - exec 4>&2 -fi -# Log stderr to log file -exec 2> >(exec tee -a "${LOGFILE}" >&2) - self_update() { if [ ${DO_SELF_UPDATE} == 0 ]; then return @@ -199,7 +193,7 @@ log_info() { log_error() { TELEM_PAYLOAD="${TELEM_PAYLOAD}\nmessage=$@" log_to_file "$@" - echo -e "$@" 1>&4 + echo -e "$@" 1>&2 } bashlock() { @@ -243,7 +237,7 @@ rebuild_kdump_initrd() { test -f /usr/lib/systemd/system/kdump.service || return systemctl is-enabled --quiet kdump.service if [ $? = 0 -a -x "/.snapshots/$1/snapshot/usr/sbin/tu-rebuild-kdump-initrd" ]; then - tukit ${TUKIT_OPTS} call "$1" /usr/sbin/tu-rebuild-kdump-initrd |& tee -a ${LOGFILE} + tukit ${TUKIT_OPTS} call "$1" /usr/sbin/tu-rebuild-kdump-initrd |& tee -a ${LOGFILE} 1>&${origstdout} fi } @@ -970,7 +964,9 @@ while [ 1 ]; do ;; --quiet) VERBOSITY=1 - TUKIT_OPTS="${TUKIT_OPTS} -q" + TUKIT_OPTS="${TUKIT_OPTS} --quiet" + DRACUT_OPTS="${DRACUT_OPTS} --quiet" + # ZYPPER_ARG handled below shift ;; register) @@ -1005,6 +1001,19 @@ while [ 1 ]; do esac done +# Duplicate stdout before creating custom handlers +exec {origstdout}>&1 +exec {origstderr}>&2 + +# Log stderr to log file in case anything goes wrong within transactional-update +exec 2> >(exec tee -a "${LOGFILE}" >&2) +if [ "${VERBOSITY}" -eq 1 ]; then + exec 1>/dev/null + if [ -n "${ZYPPER_ARG}" ]; then + ZYPPER_ARG="--quiet ${ZYPPER_ARG}" + fi +fi + # Setup SELinux if [ "${SETUP_SELINUX}" -eq 1 ]; then # Setting up SELinux requires several steps: @@ -1058,7 +1067,7 @@ if [ -z "${TA_UPDATE_TMPFILE}" ]; then else # Set exit handler to clean up artifacts of the self-update trap 'rm -f "$LOCKFILE" && rm -rf "${TA_UPDATE_TMPFILE}" && unset TA_UPDATE_TMPFILE' EXIT pushd "${TA_UPDATE_TMPFILE}" >/dev/null - zypper --non-interactive --pkg-cache-dir "${TA_UPDATE_TMPFILE}" download libtukit0 tukit + zypper --non-interactive --pkg-cache-dir "${TA_UPDATE_TMPFILE}" download libtukit4 tukit find . -name *.rpm -exec sh -c 'rpm2cpio {} | cpio -idmv 2>/dev/null' \; popd >/dev/null export LD_LIBRARY_PATH="${TA_UPDATE_TMPFILE}/usr/lib64:${TA_UPDATE_TMPFILE}/usr/lib" @@ -1305,7 +1314,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ UNUSED_SNAPSHOTS="${SNAPSHOT_ID} ${UNUSED_SNAPSHOTS}" if [ ${DO_REGISTRATION} -eq 1 ]; then - tukit ${TUKIT_OPTS} callext ${SNAPSHOT_ID} SUSEConnect --root {} ${REGISTRATION_ARGS} + tukit ${TUKIT_OPTS} callext ${SNAPSHOT_ID} SUSEConnect --root {} ${REGISTRATION_ARGS} |& tee -a ${LOGFILE} 1>&${origstdout} fi if [ -n "${ZYPPER_ARG}" ]; then @@ -1314,7 +1323,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ if [ ${DO_MIGRATION} -eq 1 ]; then # transactional-update migration export DISABLE_RESTART_ON_UPDATE=yes - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" zypper ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" zypper ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} 1>&${origstdout} RETVAL=${PIPESTATUS[0]} else if [ ${DO_CALLEXT} -eq 1 ]; then @@ -1337,7 +1346,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ fi export DISABLE_RESTART_ON_UPDATE=yes - ${zypper_cmd} ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} + ${zypper_cmd} ${ZYPPER_ARG} ${ZYPPER_NONINTERACTIVE} "${ZYPPER_ARG_PKGS[@]}" |& tee -a ${LOGFILE} 1>&${origstdout} RETVAL=${PIPESTATUS[0]} if [ \( $RETVAL -eq 0 -o $RETVAL -eq 102 -o $RETVAL -eq 103 \) -a -n "${INCLUDES_KERNEL_PACKAGES}" ]; then ${zypper_cmd} -n purge-kernels |& tee -a ${LOGFILE} @@ -1413,7 +1422,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ if [ ${REWRITE_INITRD} -eq 1 ]; then log_info "Creating new initrd" - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" dracut --force --regenerate-all + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" dracut ${DRACUT_OPTS} --force --regenerate-all |& tee -a ${LOGFILE} 1>&${origstdout} if [ $? -ne 0 ]; then log_error "ERROR: initrd creation failed!" EXITCODE=1 @@ -1429,7 +1438,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ if [ ${REWRITE_GRUB_CFG} -eq 1 ]; then log_info "Creating a new grub2 config" - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash -c "/usr/sbin/grub2-mkconfig > /boot/grub2/grub.cfg" + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash -c "/usr/sbin/grub2-mkconfig > /boot/grub2/grub.cfg" |& tee -a ${LOGFILE} 1>&${origstdout} if [ $? -ne 0 ]; then log_error "ERROR: grub2-mkconfig failed!" EXITCODE=1; @@ -1442,7 +1451,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ if [ ${REWRITE_BOOTLOADER} -eq 1 ]; then log_info "Writing new bootloader" - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" /sbin/pbl --install + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" /sbin/pbl --install |& tee -a ${LOGFILE} 1>&${origstdout} if [ $? -ne 0 ]; then log_error "ERROR: /sbin/pbl --install failed!" EXITCODE=1; @@ -1450,13 +1459,13 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ fi if [ ${DO_RUN} -eq 1 ]; then - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" "${RUN_CMD[@]}" + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" "${RUN_CMD[@]}" |& tee -a ${LOGFILE} 1>&${origstdout} fi if [ ${RUN_SHELL} -eq 1 ]; then log_to_stdout "Opening chroot in snapshot ${SNAPSHOT_ID}, continue with 'exit'" export PS1="transactional update # " - tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash 2>&4 + tukit ${TUKIT_OPTS} call "${SNAPSHOT_ID}" bash 1>&${origstdout} 2>&${origstderr} fi if [ -x /usr/sbin/selinuxenabled ] && /usr/sbin/selinuxenabled ; then @@ -1476,7 +1485,7 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ echo "EXPECTED_SNAPSHOT_ID=${SNAPSHOT_ID}" > "${NEW_SNAPSHOT_FLAG}" echo "PREV_SNAPSHOT_ID=${CURRENT_SNAPSHOT_ID}" >> "${NEW_SNAPSHOT_FLAG}" fi - tukit ${TUKIT_OPTS} close "${SNAPSHOT_ID}" + tukit ${TUKIT_OPTS} close "${SNAPSHOT_ID}" |& tee -a ${LOGFILE} fi # If --drop-if-no-change is used, then the snapshot may not exist any more; @@ -1515,14 +1524,17 @@ if [ -n "${ZYPPER_ARG}" -o ${REWRITE_GRUB_CFG} -eq 1 \ if [ ${EXITCODE} -ne 0 ]; then quit ${EXITCODE} elif [ $REBOOT_AFTERWARDS -eq 0 ]; then - log_info "\nPlease reboot your machine to activate the changes and avoid data loss." + log_info "" + log_info "Please reboot your machine to activate the changes and avoid data loss." touch "${NEEDS_RESTARTING_FILE}" fi if [ "${DEFAULT_SNAPSHOT_ID}" -ne "${BASE_SNAPSHOT_ID}" ]; then - log_info "\nWARNING: This snapshot has been created from a different base (${BASE_SNAPSHOT_ID})" + log_info "" + log_info "WARNING: This snapshot has been created from a different base (${BASE_SNAPSHOT_ID})" log_info " than the previous default snapshot (${DEFAULT_SNAPSHOT_ID}) and does not" - log_info " contain the changes from the latter.\n" + log_info " contain the changes from the latter." + log_info "" fi log_info "New default snapshot is #${SNAPSHOT_ID} (${SNAPSHOT_DIR})." diff --git a/systemd/Makefile.am b/systemd/Makefile.am index 68e436a..9dd7741 100644 --- a/systemd/Makefile.am +++ b/systemd/Makefile.am @@ -5,10 +5,12 @@ systemddir = @SYSTEMDDIR@ systemd_DATA = transactional-update.timer transactional-update.service \ + transactional-update-cleanup.timer transactional-update-cleanup.service \ create-dirs-from-rpmdb.service -EXTRA_DIST = transactional-update.timer transactional-update.service.in \ +EXTRA_DIST = transactional-update.timer transactional-update-cleanup.timer \ + transactional-update.service.in transactional-update-cleanup.service.in \ create-dirs-from-rpmdb.service -CLEANFILES = transactional-update.service +CLEANFILES = transactional-update.service transactional-update-cleanup.service do_subst = sed -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ -e 's,[@]sbindir[@],$(sbindir),g' \ @@ -16,3 +18,5 @@ do_subst = sed -e 's,[@]sysconfdir[@],$(sysconfdir),g' \ transactional-update.service: transactional-update.service.in Makefile $(do_subst) < transactional-update.service.in > transactional-update.service +transactional-update-cleanup.service: transactional-update-cleanup.service.in Makefile + $(do_subst) < transactional-update-cleanup.service.in > transactional-update-cleanup.service diff --git a/systemd/transactional-update-cleanup.service.in b/systemd/transactional-update-cleanup.service.in new file mode 100644 index 0000000..abe3cb4 --- /dev/null +++ b/systemd/transactional-update-cleanup.service.in @@ -0,0 +1,7 @@ +[Unit] +Description=Mark old snapshots for deletion +Documentation=man:transactional-update(8) + +[Service] +Type=oneshot +ExecStart=@sbindir@/transactional-update cleanup diff --git a/systemd/transactional-update-cleanup.timer b/systemd/transactional-update-cleanup.timer new file mode 100644 index 0000000..c0676ec --- /dev/null +++ b/systemd/transactional-update-cleanup.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Daily cleanup of the system +Documentation=man:transactional-update(8) + +[Timer] +OnCalendar=*-*-* 23:00:00 + +[Install] +WantedBy=timers.target diff --git a/systemd/transactional-update.timer b/systemd/transactional-update.timer index 4b8a2a5..38fbe3c 100644 --- a/systemd/transactional-update.timer +++ b/systemd/transactional-update.timer @@ -7,7 +7,6 @@ After=network.target local-fs.target OnCalendar=daily AccuracySec=1m RandomizedDelaySec=2h -#Persistent=true [Install] WantedBy=timers.target