Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add a plugin mechanism to tukit #122

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions doc/tukit-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Tukit plugins

## Motivation

Sometimes it is useful to inspect the content of a transaction in
certain points. For example, before `transactional-update dup` we
want to add new files into the system, and after the update we want to
collect the list of new packages.

With a plugin system we can provide scripts that can be executed
before or after each action supported by `transactional-update`, like
package installation, creation of new `initrd`, etc. This can work
for most of the use cases, but there are certain tasks that cannot be
done at this level.

One example of those tasks is inspecting the `/run` directory after a
full system upgrade to look for files that can signalize a condition,
like the one that will trigger the `initrd` creation in a
post-transaction scriptlet. The `/run` directory that is alive inside
the new transaction is different from the one running in the host, but
in both cases it is a `tmpfs` filesystem. This means that every time
that it is unmounted we lost the information stored in there. Sadly,
every time that `transactional-update` calls `tukit` to realize an
action inside the snapshot, those directories are mounted and
unmounted.

The solution for this is to have the plugins at the `tukit` level.

## Directories and shadowing

The plugins can be installed in two different places:

* `/usr/lib/tukit/plugins`: place for plugins that comes from packages
* `/etc/tukit/plugins`: for user defined plugins

The plugins in `/etc` can shadow the ones from `/usr` using the same
name. For example, if the plugin `get_status` is in both places with
the executable attribute, `tukit` will use the code from `/etc`,
shadowing the one from the packages.

One variation of shadowing is when the plugin in `/etc` is a soft link
to `/dev/null`. This will be used as a mark to completely disable
this plugin, and would not be called by `tukit`.

The plugins in `/etc` will be called before the ones in `/usr` but the
user should not depend on the calling order.

## Stages

The actions are based on the low-level API of libtukit, not of the
user API level. This means that some verbs like `tukit execute <cmd>`
will be presented as several of those low level actions: create
snapshot, execute command, keep snapshot.

Some actions will trigger a plugin call before and after the action
itself, depending if it makes sense in the context of this action.

The next table summarizes the action, the stage and different
parameters sent to the plugin.

| Action | Stage | Parameters | Notes |
|----------|-------|-----------------------------------|-------|
| init | -pre | | |
| | -post | path, snapshot\_id | |
| resume | -pre | snapshot\_id | |
| | -post | path, snapshot\_id | |
| execute | -pre | path, snapshot\_id, action params | |
| | -post | path, snapshot\_id, action params | |
| callExt | -pre | path, snapshot\_id, action params | [1] |
| | -post | path, snapshot\_id, action params | |
aplanas marked this conversation as resolved.
Show resolved Hide resolved
| finalize | -pre | path, snapshot\_id | |
| | -post | snapshot\_id, [discarded] | [2] |
| abort | -post | snapshot\_id | [3] |
| keep | -pre | path, snapshot\_id | |
| | -post | snapshot\_id | |
| reboot | -pre | | |

aplanas marked this conversation as resolved.
Show resolved Hide resolved
[1] The {} placeholder gets expanded in the arguments passed to the
plugin

[2] If the snapshot is discarded, the second parameter for the -post
is "discarded"

[3] abort-pre cannot be captured from the libtukit level


## Example

```bash
#!/bin/bash

exec_pre() {
local path="$1"; shift
local snapshot_id="$1"; shift
local cmd="$@"

# The live snapshot is in "$path", and the future closed snapshot in
# "/.snapshots/${snapshot_id}/snapshot

mkdir -p /var/lib/report
echo "${snapshot_id}: $cmd" >> /var/lib/report/all_commands
}

declare -A commands

commands['execute-pre']=exec_pre
commands['callExt-pre']=exec_pre

cmd="$1"
shift
[ -n "$cmd" ] || cmd=help
if [ "${#commands[$cmd]}" -gt 0 ]; then
${commands[$cmd]} "$@"
fi
```
4 changes: 2 additions & 2 deletions lib/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ lib_LTLIBRARIES = libtukit.la
libtukit_la_SOURCES=Transaction.cpp \
SnapshotManager.cpp Snapshot/Snapper.cpp \
Mount.cpp Overlay.cpp Reboot.cpp Configuration.cpp \
Util.cpp Supplement.cpp Bindings/CBindings.cpp
Util.cpp Supplement.cpp Plugins.cpp Bindings/CBindings.cpp
publicheadersdir=$(includedir)/tukit
publicheaders_HEADERS=Transaction.hpp \
SnapshotManager.hpp Reboot.hpp \
Bindings/libtukit.h
noinst_HEADERS=Snapshot/Snapper.hpp Snapshot.hpp \
Mount.hpp Overlay.hpp Log.hpp Configuration.hpp \
Util.hpp Supplement.hpp Exceptions.hpp
Util.hpp Supplement.hpp Exceptions.hpp Plugins.hpp
libtukit_la_CPPFLAGS=-DPREFIX=\"$(prefix)\" -DCONFDIR=\"$(sysconfdir)\" $(ECONF_CFLAGS) $(LIBMOUNT_CFLAGS) $(SELINUX_CFLAGS)
libtukit_la_LDFLAGS=$(ECONF_LIBS) $(LIBMOUNT_LIBS) $(SELINUX_LIBS) \
-version-info $(LIBTOOL_CURRENT):$(LIBTOOL_REVISION):$(LIBTOOL_AGE)
91 changes: 91 additions & 0 deletions lib/Plugins.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* SPDX-FileCopyrightText: 2024 SUSE LLC */

/* Plugin mechanism for tukit */

#include "Exceptions.hpp"
#include "Log.hpp"
#include "Plugins.hpp"
#include "Util.hpp"
#include <set>
#include <unistd.h>

namespace TransactionalUpdate {

using namespace std;

Plugins::Plugins(TransactionalUpdate::Transaction* transaction): transaction(transaction) {
set<string> plugins_set{};

const filesystem::path plugins_dir{filesystem::path(CONFDIR)/"tukit"/"plugins"};
const filesystem::path system_plugins_dir{filesystem::path(PREFIX)/"lib"/"tukit"/"plugins"};

for (auto d: {plugins_dir, system_plugins_dir}) {
if (!filesystem::exists(d))
continue;

for (auto const& dir_entry: filesystem::directory_iterator{d}) {
auto path = dir_entry.path();
auto filename = dir_entry.path().filename();

// Plugins can be shadowed, so a plugin in /etc can
// replace one from /usr/lib
if (plugins_set.count(filename) != 0)
continue;

// If is a symlink to /dev/null, ignore and shadow it
if (filesystem::is_symlink(path) && filesystem::read_symlink(path) == "/dev/null") {
plugins_set.insert(filename);
continue;
}

// If the plugin is not executable, ignore it
if (!(filesystem::is_regular_file(path) && (access(path.c_str(), X_OK) == 0)))
continue;

tulog.info("Found plugin ", path);
plugins.push_back(path);
plugins_set.insert(filename);
}
}
}

Plugins::~Plugins() {
plugins.clear();
}

void Plugins::run(string stage, string args) {
std::string output;

for (auto& p: plugins) {
std::string cmd = p.string() + " " + stage;
if (!args.empty())
cmd.append(" " + args);

try {
output = Util::exec(cmd);
if (!output.empty())
tulog.info("Output of plugin ", p, ": ", output);
} catch (const ExecutionException &e) {
// An error in the plugin should not discard the transaction
tulog.error("ERROR: Plugin ", p, " failed with ", e.what());
}
}
}

void Plugins::run(string stage, char* argv[]) {
std::string args;

if (transaction != nullptr)
args.append(transaction->getBindDir().string() + " " + transaction->getSnapshot());

int i = 0;
while (argv != nullptr && argv[i]) {
args.append(" ");
args.append(argv[i++]);
}

run(stage, args);
}

} // namespace TransactionalUpdate
29 changes: 29 additions & 0 deletions lib/Plugins.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* SPDX-FileCopyrightText: 2024 SUSE LLC */

/* Plugin mechanism for tukit */

#ifndef T_U_PLUGINS_H
#define T_U_PLUGINS_H

#include "Transaction.hpp"
#include <filesystem>
#include <string>
#include <vector>

namespace TransactionalUpdate {

class Plugins {
public:
Plugins(TransactionalUpdate::Transaction* transaction);
virtual ~Plugins();
void run(std::string stage, std::string args);
void run(std::string stage, char* argv[]);
protected:
TransactionalUpdate::Transaction* transaction;
std::vector<std::filesystem::path> plugins;
};

} // namespace TransactionalUpdate

#endif // T_U_PLUGINS_H
3 changes: 3 additions & 0 deletions lib/Reboot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "Configuration.hpp"
#include "Exceptions.hpp"
#include "Log.hpp"
#include "Plugins.hpp"
#include "Snapshot.hpp"
#include "SnapshotManager.hpp"
#include "Util.hpp"
Expand Down Expand Up @@ -77,6 +78,8 @@ Reboot::Reboot(std::string method) {
}

void Reboot::reboot() {
TransactionalUpdate::Plugins plugins{nullptr};
plugins.run("reboot-pre", nullptr);
Util::exec(command);
}

Expand Down
Loading