Переглянути джерело

add ucitrigger: a uci plugin, command line tool and lua interface for automatically applying uci config changes

SVN-Revision: 16375
Felix Fietkau 16 роки тому
батько
коміт
e7cbce1f9e

+ 19 - 1
package/uci/Makefile

@@ -36,10 +36,17 @@ define Package/uci
   TITLE:=Utility for the Unified Configuration Interface (UCI)
 endef
 
+define Package/ucitrigger
+  SECTION:=base
+  CATEGORY:=Base system
+  DEPENDS:=+libuci-lua
+  TITLE:=Automatic triggers for applying system config changes
+endef
+
 define Package/libuci-lua
   SECTION=libs
   CATEGORY=Libraries
-  DEPENDS:=+libuci +lua
+  DEPENDS:=+libuci +liblua
   TITLE:=Lua plugin for UCI
 endef
 
@@ -63,6 +70,8 @@ endif
 define Build/Compile
 	$(MAKE) -C $(PKG_BUILD_DIR) $(UCI_MAKEOPTS)
 	$(MAKE) -C $(PKG_BUILD_DIR)/lua $(UCI_MAKEOPTS)
+	$(MAKE) -C $(PKG_BUILD_DIR)/trigger $(UCI_MAKEOPTS) \
+		LIBS="$(TARGET_LDFLAGS) -L$(PKG_BUILD_DIR) -luci -llua -lcrypt -lm"
 endef
 
 define Package/libuci/install
@@ -75,6 +84,14 @@ define Package/libuci-lua/install
 	$(CP) $(PKG_BUILD_DIR)/lua/uci.so $(1)/usr/lib/lua/
 endef
 
+define Package/ucitrigger/install
+	$(INSTALL_DIR) $(1)/usr/lib/lua/uci $(1)/lib/config/trigger $(1)/usr/sbin
+	$(INSTALL_DATA) ./trigger/lib/trigger.lua $(1)/usr/lib/lua/uci/
+	$(INSTALL_DATA) ./trigger/modules/*.lua $(1)/lib/config/trigger/
+	$(INSTALL_DATA) $(PKG_BUILD_DIR)/trigger/uci_trigger.so $(1)/usr/lib/
+	$(INSTALL_BIN) ./trigger/apply_config $(1)/usr/sbin/
+endef
+
 define Package/uci/install
 	$(INSTALL_DIR) $(1)/etc/uci-defaults
 	$(INSTALL_DIR) $(1)/sbin
@@ -94,3 +111,4 @@ endef
 $(eval $(call BuildPackage,uci))
 $(eval $(call BuildPackage,libuci))
 $(eval $(call BuildPackage,libuci-lua))
+$(eval $(call BuildPackage,ucitrigger))

+ 423 - 0
package/uci/patches/110-plugin_support.patch

@@ -0,0 +1,423 @@
+--- a/Makefile
++++ b/Makefile
+@@ -7,7 +7,7 @@ DEBUG_TYPECAST=0
+ 
+ include Makefile.inc
+ 
+-LIBS=-lc
++LIBS=-lc -ldl
+ SHLIB_FILE=libuci.$(SHLIB_EXT).$(VERSION)
+ 
+ define add_feature
+@@ -23,6 +23,7 @@ ucimap.o: ucimap.c uci.h uci_config.h uc
+ 
+ uci_config.h: FORCE
+ 	@rm -f "[email protected]"
++	@echo "#define UCI_PREFIX \"$(prefix)\"" > "[email protected]"
+ 	$(call add_feature,PLUGIN_SUPPORT)
+ 	$(call add_feature,DEBUG)
+ 	$(call add_feature,DEBUG_TYPECAST)
+@@ -33,10 +34,10 @@ uci_config.h: FORCE
+ 	fi
+ 
+ uci: cli.o libuci.$(SHLIB_EXT)
+-	$(CC) -o $@ $< -L. -luci
++	$(CC) -o $@ $< -L. -luci $(LIBS)
+ 
+ uci-static: cli.o libuci.a
+-	$(CC) $(CFLAGS) -o $@ $^
++	$(CC) $(CFLAGS) -o $@ $^ $(LIBS)
+ 
+ libuci-static.o: libuci.c $(LIBUCI_DEPS)
+ 	$(CC) $(CFLAGS) -c -o $@ $<
+--- a/cli.c
++++ b/cli.c
+@@ -27,6 +27,7 @@ static enum {
+ 	CLI_FLAG_NOCOMMIT = (1 << 2),
+ 	CLI_FLAG_BATCH =    (1 << 3),
+ 	CLI_FLAG_SHOW_EXT = (1 << 4),
++	CLI_FLAG_NOPLUGINS= (1 << 5),
+ } flags;
+ 
+ static FILE *input;
+@@ -136,6 +137,7 @@ static void uci_usage(void)
+ 		"\t-c <path>  set the search path for config files (default: /etc/config)\n"
+ 		"\t-d <str>   set the delimiter for list values in uci show\n"
+ 		"\t-f <file>  use <file> as input instead of stdin\n"
++		"\t-L         do not load any plugins\n"
+ 		"\t-m         when importing, merge data into an existing package\n"
+ 		"\t-n         name unnamed sections on export (default)\n"
+ 		"\t-N         don't name unnamed sections\n"
+@@ -603,7 +605,7 @@ int main(int argc, char **argv)
+ 		return 1;
+ 	}
+ 
+-	while((c = getopt(argc, argv, "c:d:f:mnNp:P:sSqX")) != -1) {
++	while((c = getopt(argc, argv, "c:d:f:LmnNp:P:sSqX")) != -1) {
+ 		switch(c) {
+ 			case 'c':
+ 				uci_set_confdir(ctx, optarg);
+@@ -618,6 +620,9 @@ int main(int argc, char **argv)
+ 					return 1;
+ 				}
+ 				break;
++			case 'L':
++				flags |= CLI_FLAG_NOPLUGINS;
++				break;
+ 			case 'm':
+ 				flags |= CLI_FLAG_MERGE;
+ 				break;
+@@ -662,6 +667,10 @@ int main(int argc, char **argv)
+ 		uci_usage();
+ 		return 0;
+ 	}
++
++	if (!(flags & CLI_FLAG_NOPLUGINS))
++		uci_load_plugins(ctx, NULL);
++
+ 	ret = uci_cmd(argc - 1, argv + 1);
+ 	if (input != stdin)
+ 		fclose(input);
+--- a/history.c
++++ b/history.c
+@@ -406,6 +406,17 @@ int uci_save(struct uci_context *ctx, st
+ 	if ((asprintf(&filename, "%s/%s", ctx->savedir, p->e.name) < 0) || !filename)
+ 		UCI_THROW(ctx, UCI_ERR_MEM);
+ 
++	uci_foreach_element(&ctx->hooks, tmp) {
++		struct uci_hook *hook = uci_to_hook(tmp);
++
++		if (!hook->ops->set)
++			continue;
++
++		uci_foreach_element(&p->history, e) {
++			hook->ops->set(hook->ops, p, uci_to_history(e));
++		}
++	}
++
+ 	ctx->err = 0;
+ 	UCI_TRAP_SAVE(ctx, done);
+ 	f = uci_open_stream(ctx, filename, SEEK_END, true, true);
+--- a/libuci.c
++++ b/libuci.c
+@@ -22,6 +22,8 @@
+ #include <string.h>
+ #include <stdlib.h>
+ #include <stdio.h>
++#include <dlfcn.h>
++#include <glob.h>
+ #include "uci.h"
+ 
+ static const char *uci_confdir = UCI_CONFDIR;
+@@ -39,6 +41,7 @@ static const char *uci_errstr[] = {
+ };
+ 
+ static void uci_cleanup(struct uci_context *ctx);
++static void uci_unload_plugin(struct uci_context *ctx, struct uci_plugin *p);
+ 
+ #include "uci_internal.h"
+ #include "util.c"
+@@ -56,6 +59,8 @@ struct uci_context *uci_alloc_context(vo
+ 	uci_list_init(&ctx->root);
+ 	uci_list_init(&ctx->history_path);
+ 	uci_list_init(&ctx->backends);
++	uci_list_init(&ctx->hooks);
++	uci_list_init(&ctx->plugins);
+ 	ctx->flags = UCI_FLAG_STRICT | UCI_FLAG_SAVED_HISTORY;
+ 
+ 	ctx->confdir = (char *) uci_confdir;
+@@ -86,6 +91,9 @@ void uci_free_context(struct uci_context
+ 		uci_free_element(e);
+ 	}
+ 	UCI_TRAP_RESTORE(ctx);
++	uci_foreach_element_safe(&ctx->root, tmp, e) {
++		uci_unload_plugin(ctx, uci_to_plugin(e));
++	}
+ 	free(ctx);
+ 
+ ignore:
+@@ -209,9 +217,16 @@ int uci_commit(struct uci_context *ctx, 
+ int uci_load(struct uci_context *ctx, const char *name, struct uci_package **package)
+ {
+ 	struct uci_package *p;
++	struct uci_element *e;
++
+ 	UCI_HANDLE_ERR(ctx);
+ 	UCI_ASSERT(ctx, ctx->backend && ctx->backend->load);
+ 	p = ctx->backend->load(ctx, name);
++	uci_foreach_element(&ctx->hooks, e) {
++		struct uci_hook *h = uci_to_hook(e);
++		if (h->ops->load)
++			h->ops->load(h->ops, p);
++	}
+ 	if (package)
+ 		*package = p;
+ 
+@@ -280,3 +295,94 @@ int uci_set_backend(struct uci_context *
+ 	ctx->backend = uci_to_backend(e);
+ 	return 0;
+ }
++
++int uci_add_hook(struct uci_context *ctx, const struct uci_hook_ops *ops)
++{
++	struct uci_element *e;
++	struct uci_hook *h;
++
++	UCI_HANDLE_ERR(ctx);
++
++	/* check for duplicate elements */
++	uci_foreach_element(&ctx->hooks, e) {
++		h = uci_to_hook(e);
++		if (h->ops == ops)
++			return UCI_ERR_INVAL;
++	}
++
++	h = uci_alloc_element(ctx, hook, "", 0);
++	h->ops = ops;
++	uci_list_init(&h->e.list);
++	uci_list_add(&ctx->hooks, &h->e.list);
++
++	return 0;
++}
++
++int uci_remove_hook(struct uci_context *ctx, const struct uci_hook_ops *ops)
++{
++	struct uci_element *e;
++
++	uci_foreach_element(&ctx->hooks, e) {
++		struct uci_hook *h = uci_to_hook(e);
++		if (h->ops == ops) {
++			uci_list_del(&e->list);
++			return 0;
++		}
++	}
++	return UCI_ERR_NOTFOUND;
++}
++
++int uci_load_plugin(struct uci_context *ctx, const char *filename)
++{
++	struct uci_plugin *p;
++	const struct uci_plugin_ops *ops;
++	void *dlh;
++
++	UCI_HANDLE_ERR(ctx);
++	dlh = dlopen(filename, RTLD_GLOBAL|RTLD_NOW);
++	if (!dlh)
++		UCI_THROW(ctx, UCI_ERR_NOTFOUND);
++
++	ops = dlsym(dlh, "uci_plugin");
++	if (!ops || !ops->attach || (ops->attach(ctx) != 0)) {
++		if (!ops)
++			fprintf(stderr, "No ops\n");
++		else if (!ops->attach)
++			fprintf(stderr, "No attach\n");
++		else
++			fprintf(stderr, "Other weirdness\n");
++		dlclose(dlh);
++		UCI_THROW(ctx, UCI_ERR_INVAL);
++	}
++
++	p = uci_alloc_element(ctx, plugin, filename, 0);
++	p->dlh = dlh;
++	p->ops = ops;
++	uci_list_add(&ctx->plugins, &p->e.list);
++
++	return 0;
++}
++
++static void uci_unload_plugin(struct uci_context *ctx, struct uci_plugin *p)
++{
++	if (p->ops->detach)
++		p->ops->detach(ctx);
++	dlclose(p->dlh);
++	uci_free_element(&p->e);
++}
++
++int uci_load_plugins(struct uci_context *ctx, const char *pattern)
++{
++	glob_t gl;
++	int i;
++
++	if (!pattern)
++		pattern = UCI_PREFIX "/lib/uci_*.so";
++
++	memset(&gl, 0, sizeof(gl));
++	glob(pattern, 0, NULL, &gl);
++	for (i = 0; i < gl.gl_pathc; i++)
++		uci_load_plugin(ctx, gl.gl_pathv[i]);
++
++	return 0;
++}
+--- a/uci.h
++++ b/uci.h
+@@ -56,6 +56,8 @@ struct uci_list
+ };
+ 
+ struct uci_ptr;
++struct uci_plugin;
++struct uci_hook_ops;
+ struct uci_element;
+ struct uci_package;
+ struct uci_section;
+@@ -275,6 +277,43 @@ extern int uci_set_backend(struct uci_co
+  */
+ extern bool uci_validate_text(const char *str);
+ 
++
++/**
++ * uci_add_hook: add a uci hook
++ * @ctx: uci context
++ * @ops: uci hook ops
++ *
++ * NB: allocated and freed by the caller
++ */
++extern int uci_add_hook(struct uci_context *ctx, const struct uci_hook_ops *ops);
++
++/**
++ * uci_remove_hook: remove a uci hook
++ * @ctx: uci context
++ * @ops: uci hook ops
++ */
++extern int uci_remove_hook(struct uci_context *ctx, const struct uci_hook_ops *ops);
++
++/**
++ * uci_load_plugin: load an uci plugin
++ * @ctx: uci context
++ * @filename: path to the uci plugin
++ *
++ * NB: plugin will be unloaded automatically when the context is freed
++ */
++int uci_load_plugin(struct uci_context *ctx, const char *filename);
++
++/**
++ * uci_load_plugins: load all uci plugins from a directory
++ * @ctx: uci context
++ * @pattern: pattern of uci plugin files (optional)
++ *
++ * if pattern is NULL, then uci_load_plugins will call uci_load_plugin
++ * for uci_*.so in <prefix>/lib/
++ */
++int uci_load_plugins(struct uci_context *ctx, const char *pattern);
++
++
+ /* UCI data structures */
+ enum uci_type {
+ 	UCI_TYPE_UNSPEC = 0,
+@@ -285,6 +324,8 @@ enum uci_type {
+ 	UCI_TYPE_PATH = 5,
+ 	UCI_TYPE_BACKEND = 6,
+ 	UCI_TYPE_ITEM = 7,
++	UCI_TYPE_HOOK = 8,
++	UCI_TYPE_PLUGIN = 9,
+ };
+ 
+ enum uci_option_type {
+@@ -346,6 +387,9 @@ struct uci_context
+ 	bool internal, nested;
+ 	char *buf;
+ 	int bufsz;
++
++	struct uci_list hooks;
++	struct uci_list plugins;
+ };
+ 
+ struct uci_package
+@@ -420,6 +464,31 @@ struct uci_ptr
+ 	const char *value;
+ };
+ 
++struct uci_hook_ops
++{
++	void (*load)(const struct uci_hook_ops *ops, struct uci_package *p);
++	void (*set)(const struct uci_hook_ops *ops, struct uci_package *p, struct uci_history *e);
++};
++
++struct uci_hook
++{
++	struct uci_element e;
++	const struct uci_hook_ops *ops;
++};
++
++struct uci_plugin_ops
++{
++	int (*attach)(struct uci_context *ctx);
++	void (*detach)(struct uci_context *ctx);
++};
++
++struct uci_plugin
++{
++	struct uci_element e;
++	const struct uci_plugin_ops *ops;
++	void *dlh;
++};
++
+ 
+ /* linked list handling */
+ #ifndef offsetof
+@@ -490,6 +559,8 @@ struct uci_ptr
+ #define uci_type_package UCI_TYPE_PACKAGE
+ #define uci_type_section UCI_TYPE_SECTION
+ #define uci_type_option UCI_TYPE_OPTION
++#define uci_type_hook UCI_TYPE_HOOK
++#define uci_type_plugin UCI_TYPE_PLUGIN
+ 
+ /* element typecasting */
+ #ifdef UCI_DEBUG_TYPECAST
+@@ -499,6 +570,8 @@ static const char *uci_typestr[] = {
+ 	[uci_type_package] = "package",
+ 	[uci_type_section] = "section",
+ 	[uci_type_option] = "option",
++	[uci_type_hook] = "hook",
++	[uci_type_plugin] = "plugin",
+ };
+ 
+ static void uci_typecast_error(int from, int to)
+@@ -520,6 +593,8 @@ BUILD_CAST(history)
+ BUILD_CAST(package)
+ BUILD_CAST(section)
+ BUILD_CAST(option)
++BUILD_CAST(hook)
++BUILD_CAST(plugin)
+ 
+ #else
+ #define uci_to_backend(ptr) container_of(ptr, struct uci_backend, e)
+@@ -527,6 +602,8 @@ BUILD_CAST(option)
+ #define uci_to_package(ptr) container_of(ptr, struct uci_package, e)
+ #define uci_to_section(ptr) container_of(ptr, struct uci_section, e)
+ #define uci_to_option(ptr)  container_of(ptr, struct uci_option, e)
++#define uci_to_hook(ptr)    container_of(ptr, struct uci_hook, e)
++#define uci_to_plugin(ptr)  container_of(ptr, struct uci_plugin, e)
+ #endif
+ 
+ /**
+--- a/lua/uci.c
++++ b/lua/uci.c
+@@ -765,6 +765,20 @@ uci_lua_add_history(lua_State *L)
+ }
+ 
+ static int
++uci_lua_load_plugins(lua_State *L)
++{
++	struct uci_context *ctx;
++	int ret, offset = 0;
++	const char *str = NULL;
++
++	ctx = find_context(L, &offset);
++	if (lua_isstring(L, -1))
++		str = lua_tostring(L, -1);
++	ret = uci_load_plugins(ctx, str);
++	return uci_push_status(L, ctx, false);
++}
++
++static int
+ uci_lua_set_savedir(lua_State *L)
+ {
+ 	struct uci_context *ctx;
+@@ -831,6 +845,7 @@ static const luaL_Reg uci[] = {
+ 	{ "changes", uci_lua_changes },
+ 	{ "foreach", uci_lua_foreach },
+ 	{ "add_history", uci_lua_add_history },
++	{ "load_plugins", uci_lua_load_plugins },
+ 	{ "get_confdir", uci_lua_get_confdir },
+ 	{ "set_confdir", uci_lua_set_confdir },
+ 	{ "get_savedir", uci_lua_get_savedir },

+ 182 - 0
package/uci/patches/120-uci_trigger.patch

@@ -0,0 +1,182 @@
+--- /dev/null
++++ b/trigger/Makefile
+@@ -0,0 +1,44 @@
++include ../Makefile.inc
++LUA_VERSION=5.1
++PREFIX_SEARCH=/usr /usr/local /opt/local
++LUA_PLUGINDIR=$(firstword \
++	$(foreach ldir,$(subst ;, ,$(shell lua -e 'print(package.cpath)')), \
++		$(if $(findstring lib/lua/,$(ldir)),$(patsubst %/?.so,%,$(ldir))) \
++	) \
++)
++
++# find lua prefix
++LUA_PREFIX=$(firstword \
++	$(foreach prefix,$(PREFIX_SEARCH),\
++		$(if $(wildcard $(prefix)/include/lua.h),$(prefix)) \
++	) \
++)
++
++libdir=$(prefix)/libs
++luadir=$(if $(LUA_PLUGINDIR),$(LUA_PLUGINDIR),$(libdir)/lua/$(LUA_VERSION))
++luainc=$(shell pkg-config --silence-errors --cflags lua$(LUA_VERSION))
++
++CPPFLAGS=-I.. $(if $(luainc),$(luainc), -I$(LUA_PREFIX)/include)
++LIBS=-L.. -luci $(shell pkg-config --silence-errors --libs lua$(LUA_VERSION))
++
++PLUGIN_LD=$(CC)
++ifeq ($(OS),Darwin)
++  PLUGIN_LDFLAGS=-bundle
++else
++  PLUGIN_LDFLAGS=-shared -Wl,-soname,$(SHLIB_FILE)
++endif
++
++all: uci_trigger.so
++
++uci_trigger.so: uci_trigger.o
++	$(PLUGIN_LD) $(PLUGIN_LDFLAGS) -o $@ $^ $(LIBS)
++
++%.o: %.c
++	$(CC) $(CPPFLAGS) $(CFLAGS) $(FPIC) -c -o $@ $<
++
++install:
++	mkdir -p $(DESTDIR)$(luadir)
++	$(INSTALL) -m0644 uci_trigger.so $(DESTDIR)$(luadir)/
++
++clean:
++	rm -f *.so *.o uci_trigger.so
+--- /dev/null
++++ b/trigger/uci_trigger.c
+@@ -0,0 +1,132 @@
++#include <sys/types.h>
++#include <sys/time.h>
++#include <stdbool.h>
++#include <string.h>
++#include <stdio.h>
++#include <lualib.h>
++#include <lauxlib.h>
++#include "uci.h"
++
++// #define DEBUG
++
++static int refcount = 0;
++static lua_State *gL = NULL;
++static bool prepared = false;
++
++struct trigger_set_op {
++	struct uci_package *p;
++	struct uci_history *h;
++};
++
++static int trigger_set(lua_State *L)
++{
++	struct trigger_set_op *so =
++		(struct trigger_set_op *)lua_touserdata(L, 1);
++	struct uci_package *p = so->p;
++	struct uci_history *h = so->h;
++	struct uci_context *ctx = p->ctx;
++
++	/* ignore non-standard savedirs/configdirs
++	 * in order to not trigger events on uci state changes */
++	if (strcmp(ctx->savedir, UCI_SAVEDIR) ||
++		strcmp(ctx->confdir, UCI_CONFDIR))
++		return 0;
++
++	if (!prepared) {
++		lua_getglobal(L, "require");
++		lua_pushstring(L, "uci.trigger");
++		lua_call(L, 1, 0);
++
++		lua_getglobal(L, "uci");
++		lua_getfield(L, -1, "trigger");
++		lua_getfield(L, -1, "load_modules");
++		lua_call(L, 0, 0);
++		prepared = true;
++	} else {
++		lua_getglobal(L, "uci");
++		lua_getfield(L, -1, "trigger");
++	}
++
++	lua_getfield(L, -1, "set");
++	lua_createtable(L, 4, 0);
++
++	lua_pushstring(L, p->e.name);
++	lua_rawseti(L, -2, 1);
++	if (h->section) {
++		lua_pushstring(L, h->section);
++		lua_rawseti(L, -2, 2);
++	}
++	if (h->e.name) {
++		lua_pushstring(L, h->e.name);
++		lua_rawseti(L, -2, 3);
++	}
++	if (h->value) {
++		lua_pushstring(L, h->value);
++		lua_rawseti(L, -2, 4);
++	}
++	lua_call(L, 1, 0);
++	lua_pop(L, 2);
++	return 0;
++}
++
++#ifdef DEBUG
++
++static int report (lua_State *L, int status) {
++	if (status && !lua_isnil(L, -1)) {
++		const char *msg = lua_tostring(L, -1);
++		if (msg == NULL) msg = "(error object is not a string)";
++		fprintf(stderr, "ERROR: %s\n", msg);
++		lua_pop(L, 1);
++	}
++	return status;
++}
++
++#else
++
++static inline int report(lua_State *L, int status) {
++	return status;
++}
++
++#endif
++
++static void trigger_set_hook(const struct uci_hook_ops *ops, struct uci_package *p, struct uci_history *h)
++{
++	struct trigger_set_op so;
++
++	so.p = p;
++	so.h = h;
++	report(gL, lua_cpcall(gL, &trigger_set, &so));
++}
++
++static struct uci_hook_ops hook = {
++	.set = trigger_set_hook,
++};
++
++static int trigger_attach(struct uci_context *ctx)
++{
++	if (!gL) {
++		gL = luaL_newstate();
++		if (!gL)
++			return -1;
++		luaL_openlibs(gL);
++
++		refcount++;
++	}
++	uci_add_hook(ctx, &hook);
++	return 0;
++}
++
++static void trigger_detach(struct uci_context *ctx)
++{
++	if (gL && (--refcount <= 0)) {
++		lua_close(gL);
++		gL = NULL;
++		refcount = 0;
++		prepared = false;
++	}
++}
++
++struct uci_plugin_ops uci_plugin = {
++	.attach = trigger_attach,
++	.detach = trigger_detach,
++};

+ 44 - 0
package/uci/trigger/apply_config

@@ -0,0 +1,44 @@
+#!/usr/bin/lua
+require("uci")
+require("uci.trigger")
+
+function usage() 
+	print("Usage: " .. arg[0] .. " [options]")
+	print("Options:")
+	print("    -a: apply the config changes")
+	print("    -t: show matching UCI triggers")
+	print("    -s: show information about tasks to be executed")
+	print("    -r: reset all triggers")
+	print("")
+end
+
+if arg[1] == "-s" then
+	local triggers = uci.trigger.get_active()
+	if #triggers > 0 then
+		print("Tasks:")
+		for i, a in ipairs(triggers) do
+			local trigger = a[1]
+			local sections = a[2]
+			print(" - " .. uci.trigger.get_description(trigger, sections))
+		end
+	else
+		print "Nothing to do"
+	end
+elseif arg[1] == "-t" then
+	local triggers = uci.trigger.get_active()
+	for i, a in ipairs(triggers) do
+		local trigger = a[1]
+		local sections = a[2]
+		if trigger.section_only then
+			print(trigger.id .. " " .. table.concat(" ", sections))
+		else
+			print(trigger.id)
+		end
+	end
+elseif arg[1] == "-a" then
+	uci.trigger.run()
+elseif arg[1] == "-r" then
+	uci.trigger.reset_state()
+else
+	usage()
+end

+ 371 - 0
package/uci/trigger/lib/trigger.lua

@@ -0,0 +1,371 @@
+module("uci.trigger", package.seeall)
+require("posix")
+require("uci")
+
+local path = "/lib/config/trigger"
+local triggers = nil
+local tmp_cursor = nil
+
+function load_modules()
+	if triggers ~= nil then
+		return
+	end
+	triggers = {
+		list = {},
+		uci = {},
+		active = {}
+	}
+	local modules = posix.glob(path .. "/*.lua")
+	if modules == nil then
+		return
+	end
+	local oldpath = package.path
+	package.path = path .. "/?.lua"
+	for i, v in ipairs(modules) do
+		pcall(require(string.gsub(v, path .. "/(%w+)%.lua$", "%1")))
+	end
+	package.path = oldpath
+end
+
+function check_table(table, name)
+	if table[name] == nil then
+		table[name] = {}
+	end
+	return table[name]
+end
+
+function get_table_val(val, vtype)
+	if type(val) == (vtype or "string") then
+		return { val }
+	elseif type(val) == "table" then
+		return val
+	end
+	return nil
+end
+
+function get_name_list(name)
+	return get_table_val(name or ".all")
+end
+
+function add_trigger_option(list, t)
+	local name = get_name_list(t.option)
+	for i, n in ipairs(name) do
+		option = check_table(list, n)
+		table.insert(option, t)
+	end
+end
+
+function add_trigger_section(list, t)
+	local name = get_name_list(t.section)
+	for i, n in ipairs(name) do
+		section = check_table(list, n)
+		add_trigger_option(section, t)
+	end
+end
+
+function check_insert_triggers(dest, list, tuple)
+	if list == nil then
+		return
+	end
+	for i, t in ipairs(list) do
+		local add = true
+		if type(t.check) == "function" then
+			add = t.check(tuple)
+		end
+		if add then
+			dest[t.id] = t
+		end
+	end
+end
+
+function find_section_triggers(tlist, pos, tuple)
+	if pos == nil then
+		return
+	end
+	check_insert_triggers(tlist, pos[".all"], tuple)
+	if tuple.option then
+		check_insert_triggers(tlist, pos[tuple.option], tuple)
+	end
+end
+
+function check_recursion(name, seen)
+	if seen == nil then
+		seen = {}
+	end
+	if seen[name] then
+		return nil
+	end
+	seen[name] = true
+	return seen
+end
+
+
+function find_recursive_depends(list, name, seen)
+	seen = check_recursion(name, seen)
+	if not seen then
+		return
+	end
+	local bt = get_table_val(triggers.list[name].belongs_to) or {}
+	for i, n in ipairs(bt) do
+		table.insert(list, n)
+		find_recursive_depends(list, n, seen)
+	end
+end
+
+function check_trigger_depth(list, name)
+	if name == nil then
+		return
+	end
+
+	local n = list[name]
+	if n == nil then
+		return
+	end
+
+	list[name] = nil
+	return check_trigger_depth(list, n)
+end
+
+function find_triggers(tuple)
+	local pos = triggers.uci[tuple.package]
+	if pos == nil then
+		return {}
+	end
+
+	local tlist = {}
+	find_section_triggers(tlist, pos[".all"], tuple)
+	find_section_triggers(tlist, pos[tuple.section[".type"]], tuple)
+
+	for n, t in pairs(tlist) do
+		local dep = {}
+		find_recursive_depends(dep, t.id)
+		for i, depname in ipairs(dep) do
+			check_trigger_depth(tlist, depname)
+		end
+	end
+
+	local nlist = {}
+	for n, t in pairs(tlist) do
+		if t then
+			table.insert(nlist, t)
+		end
+	end
+
+	return nlist
+end
+
+function reset_state()
+	assert(io.open("/var/run/uci_trigger", "w")):close()
+	if tctx then
+		tctx:unload("uci_trigger")
+	end
+end
+
+function load_state()
+	-- make sure the config file exists before we attempt to load it
+	-- uci doesn't like loading nonexistent config files
+	local f = assert(io.open("/var/run/uci_trigger", "a")):close()
+
+	load_modules()
+	triggers.active = {}
+	if tctx then
+		tctx:unload("uci_trigger")
+	else
+		tctx = uci.cursor()
+	end
+	assert(tctx:load("/var/run/uci_trigger"))
+	tctx:foreach("uci_trigger", "trigger",
+		function(section)
+			trigger = triggers.list[section[".name"]]
+			if trigger == nil then
+				return
+			end
+
+			active = {}
+			triggers.active[trigger.id] = active
+
+			local s = get_table_val(section["sections"]) or {}
+			for i, v in ipairs(s) do
+				active[v] = true
+			end
+		end
+	)
+end
+
+function get_names(list)
+	local slist = {}
+	for name, val in pairs(list) do
+		if val then
+			table.insert(slist, name)
+		end
+	end
+	return slist
+end
+
+function check_cancel(name, seen)
+	local t = triggers.list[name]
+	local dep = get_table_val(t.belongs_to)
+	seen = check_recursion(name, seen)
+
+	if not t or not dep or not seen then
+		return false
+	end
+
+	for i, v in ipairs(dep) do
+		-- only cancel triggers for all sections
+		-- if both the current and the parent trigger
+		-- are per-section
+		local section_only = false
+		if t.section_only then
+			local tdep = triggers.list[v]
+			if tdep then
+				section_only = tdep.section_only
+			end
+		end
+
+		if check_cancel(v, seen) then
+			return true
+		end
+		if triggers.active[v] then
+			if section_only then
+				for n, active in pairs(triggers.active[v]) do
+					triggers.active[name][n] = false
+				end
+			else
+				return true
+			end
+		end
+	end
+	return false
+end
+
+-- trigger api functions
+
+function add(ts)
+	for i,t in ipairs(ts) do
+		triggers.list[t.id] = t
+		match = {}
+		if t.package then
+			local package = check_table(triggers.uci, t.package)
+			add_trigger_section(package, t)
+			triggers.list[t.id] = t
+		end
+	end
+end
+
+function set(data, cursor)
+	assert(data ~= nil)
+	if cursor == nil then
+		cursor = tmp_cursor or uci.cursor()
+		tmp_cursor = uci.cursor
+	end
+
+	local tuple = {
+		package = data[1],
+		section = data[2],
+		option = data[3],
+		value = data[4]
+	}
+	assert(cursor:load(tuple.package))
+
+	load_state()
+	local section = cursor:get_all(tuple.package, tuple.section)
+	if (section == nil) then
+		if option ~= nil then
+			return
+		end
+		section = {
+			[".type"] = value
+		}
+		if tuple.section == nil then
+			tuple.section = ""
+			section[".anonymous"] = true
+		end
+		section[".name"] = tuple.section
+	end
+	tuple.section = section
+
+	local ts = find_triggers(tuple)
+	for i, t in ipairs(ts) do
+		local active = triggers.active[t.id]
+		if not active then
+			active = {}
+			triggers.active[t.id] = active
+			tctx:set("uci_trigger", t.id, "trigger")
+		end
+		if section[".name"] then
+			active[section[".name"]] = true
+		end
+		local slist = get_names(triggers.active[t.id])
+		if #slist > 0 then
+			tctx:set("uci_trigger", t.id, "sections", slist)
+		end
+	end
+	tctx:save("uci_trigger")
+end
+
+function get_description(trigger, sections)
+	if not trigger.title then
+		return trigger.id
+	end
+	local desc = trigger.title
+	if trigger.section_only and sections and #sections > 0 then
+		desc = desc .. " (" .. table.concat(sections, ", ") .. ")"
+	end
+	return desc
+end
+
+function get_active()
+	local slist = {}
+
+	if triggers == nil then
+		load_state()
+	end
+	for name, val in pairs(triggers.active) do
+		if val and not check_cancel(name) then
+			local sections = {}
+			for name, active in pairs(triggers.active[name]) do
+				if active then
+					table.insert(sections, name)
+				end
+			end
+			table.insert(slist, { triggers.list[name], sections })
+		end
+	end
+	return slist
+end
+
+function run(ts)
+	if ts == nil then
+		ts = get_active()
+	end
+	for i, t in ipairs(ts) do
+		local trigger = t[1]
+		local sections = t[2]
+		local actions = get_table_val(trigger.action, "function") or {}
+		for ai, a in ipairs(actions) do
+			if not trigger.section_only then
+				sections = { "" }
+			end
+			for si, s in ipairs(sections) do
+				if a(s) then
+					tctx:delete("uci_trigger", trigger.id)
+					tctx:save("uci_trigger")
+				end
+			end
+		end
+	end
+end
+
+-- helper functions
+
+function system_command(arg)
+	local cmd = arg
+	return function(arg)
+		return os.execute(cmd:format(arg)) == 0
+	end
+end
+
+function service_restart(arg)
+	return system_command("/etc/init.d/" .. arg .. " restart")
+end

+ 63 - 0
package/uci/trigger/modules/base.lua

@@ -0,0 +1,63 @@
+module("trigger.base", package.seeall)
+require("uci.trigger")
+
+uci.trigger.add {
+	{
+		id = "dnsmasq_restart",
+		title = "Restart dnsmasq",
+		package = "dhcp",
+		action = uci.trigger.service_restart("dnsmasq"),
+	},
+	{
+		id = "dropbear_restart",
+		title = "Restart dropbear",
+		package = "dropbear",
+		action = uci.trigger.service_restart("dropbear"),
+	},
+	{
+		id = "fstab_restart",
+		title = "Remount filesystems",
+		package = "fstab",
+		action = uci.trigger.service_restart("fstab"),
+	},
+	{
+		id = "firewall_restart",
+		title = "Reload firewall rules",
+		package = "firewall",
+		action = uci.trigger.service_restart("firewall"),
+	},
+	{
+		id = "httpd_restart",
+		title = "Restart the http server",
+		package = "httpd",
+		action = uci.trigger.service_restart("httpd")
+	},
+	{
+		id = "led_restart",
+		title = "Reload LED settings",
+		package = "system",
+		section = "led",
+		action = uci.trigger.service_restart("led")
+	},
+	{
+		id = "network_restart",
+		title = "Restart networking and wireless",
+		package = "network",
+		action = uci.trigger.service_restart("network")
+	},
+	{
+		id = "qos_restart",
+		title = "Reload Quality of Service rules",
+		package = "qos",
+		action = uci.trigger.service_restart("qos"),
+	},
+	{
+		id = "wireless_restart",
+		title = "Restart all wireless interfaces",
+		package = "wireless",
+		section = { "wifi-device", "wifi-iface" },
+		action = uci.trigger.system_command("wifi"),
+		belongs_to = "network_restart"
+	},
+}
+