Move source code to src subdir

Change-Id: I1aa2893e2b274f8d087a0d2f126486cd4afcbdfe
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..9ad5947
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,54 @@
+AM_CPPFLAGS = \
+	$(all_includes) \
+	-I$(top_srcdir) \
+	$(NULL)
+
+AM_CFLAGS = \
+	-Wall \
+	$(LIBOSMOCORE_CFLAGS) \
+	$(LIBOSMOGSM_CFLAGS) \
+	$(LIBOSMOVTY_CFLAGS) \
+	$(LIBMNL_CFLAGS) \
+	$(NULL)
+
+AM_LDFLAGS = \
+	$(COVERAGE_LDFLAGS) \
+	$(NULL)
+
+bin_PROGRAMS = \
+	osmo-sysmon \
+	osmo-ctrl-client \
+	$(NULL)
+
+osmo_sysmon_LDADD = \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(LIBOSMOVTY_LIBS) \
+	$(LIBMNL_LIBS) \
+	$(NULL)
+
+osmo_sysmon_SOURCES = \
+	value_node.c \
+	simple_ctrl.c \
+	osysmon_ctrl.c \
+	osysmon_sysinfo.c \
+	osysmon_rtnl.c \
+	osysmon_file.c \
+	osysmon_main.c \
+	$(NULL)
+
+osmo_ctrl_client_LDADD = \
+	$(LIBOSMOCORE_LIBS) \
+	$(LIBOSMOGSM_LIBS) \
+	$(NULL)
+
+osmo_ctrl_client_SOURCES = \
+	simple_ctrl.c \
+	osmo-ctrl-client.c \
+	$(NULL)
+
+noinst_HEADERS = \
+	osysmon.h \
+	simple_ctrl.h \
+	value_node.h \
+	$(NULL)
diff --git a/src/osmo-ctrl-client.c b/src/osmo-ctrl-client.c
new file mode 100644
index 0000000..83e9b7b
--- /dev/null
+++ b/src/osmo-ctrl-client.c
@@ -0,0 +1,91 @@
+/* Simple command-line client against the Osmocom CTRL interface */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "simple_ctrl.h"
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+
+static struct log_info log_info = {};
+
+static void exit_help(void)
+{
+	printf("Usage:\n");
+	printf("\tosmo-ctrl-client HOST PORT get VARIABLE\n");
+	printf("\tosmo-ctrl-client HOST PORT set VARIABLE VALUE\n");
+	printf("\tosmo-ctrl-client HOST PORT monitor\n");
+	exit(2);
+}
+
+int main(int argc, char **argv)
+{
+	struct simple_ctrl_handle *sch;
+	const char *host;
+	uint16_t port;
+	int rc;
+
+	if (argc < 4)
+		exit_help();
+
+	host = argv[1];
+	port = atoi(argv[2]);
+
+	osmo_init_logging2(NULL, &log_info);
+
+	sch = simple_ctrl_open(NULL, host, port, 1000);
+	if (!sch)
+		exit(1);
+
+	if (!strcmp(argv[3], "get")) {
+		char *val;
+		if (argc < 5)
+			exit_help();
+		val = simple_ctrl_get(sch, argv[4]);
+		if (!val)
+			exit(2);
+		printf("%s\n", val);
+	} else if (!strcmp(argv[3], "set")) {
+		if (argc < 6)
+			exit_help();
+		rc = simple_ctrl_set(sch, argv[4], argv[5]);
+		if (rc < 0)
+			exit(1);
+	} else if (!strcmp(argv[3], "monitor")) {
+		simple_ctrl_set_timeout(sch, 0);
+		while (true) {
+			struct msgb *msg = simple_ctrl_receive(sch);
+			if (!msg)
+				exit(1);
+			printf("%s", (char *) msgb_l2(msg));
+			msgb_free(msg);
+		}
+	} else
+		exit_help();
+
+	exit(0);
+}
diff --git a/src/osysmon.h b/src/osysmon.h
new file mode 100644
index 0000000..735b11e
--- /dev/null
+++ b/src/osysmon.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <osmocom/core/select.h>
+#include <osmocom/core/utils.h>
+#include <osmocom/vty/command.h>
+
+#include <stdbool.h>
+
+#include "value_node.h"
+
+struct rtnl_client_state;
+
+struct osysmon_state {
+	struct rtnl_client_state *rcs;
+	/* list of 'struct ctrl client' */
+	struct llist_head ctrl_clients;
+	/* list of 'struct netdev' */
+	struct llist_head netdevs;
+	/* list of 'struct osysmon_file' */
+	struct llist_head files;
+};
+
+extern struct osysmon_state *g_oss;
+
+
+
+enum osysmon_vty_node {
+	CTRL_CLIENT_NODE = _LAST_OSMOVTY_NODE + 1,
+	CTRL_CLIENT_GETVAR_NODE,
+	NETDEV_NODE,
+};
+
+
+int osysmon_ctrl_go_parent(struct vty *vty);
+int osysmon_ctrl_init();
+int osysmon_ctrl_poll(struct value_node *parent);
+
+int osysmon_rtnl_go_parent(struct vty *vty);
+int osysmon_rtnl_init();
+int osysmon_rtnl_poll(struct value_node *parent);
+
+int osysmon_sysinfo_init();
+int osysmon_sysinfo_poll(struct value_node *parent);
+
+int osysmon_file_init();
+int osysmon_file_poll(struct value_node *parent);
diff --git a/src/osysmon_ctrl.c b/src/osysmon_ctrl.c
new file mode 100644
index 0000000..25c2b4c
--- /dev/null
+++ b/src/osysmon_ctrl.c
@@ -0,0 +1,304 @@
+/* Simple Osmocom System Monitor (osysmon): Support for CTRL monitoring */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <string.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+#include "osysmon.h"
+#include "simple_ctrl.h"
+#include "value_node.h"
+
+/***********************************************************************
+ * Data Model
+ ***********************************************************************/
+
+/* a single CTRL client */
+struct ctrl_client {
+	/* links to osysmon.ctrl_clients */
+	struct llist_head list;
+	struct {
+		/* name of this CTRL client */
+		const char *name;
+		/* remote host/IP */
+		const char *remote_host;
+		/* remote CTRL port */
+		uint16_t remote_port;
+	} cfg;
+	struct simple_ctrl_handle *sch;
+	/* list of ctrl_client_get_var objects */
+	struct llist_head get_vars;
+};
+
+/* a variable we are GETing via a ctrl_client */
+struct ctrl_client_get_var {
+	/* links to ctrl_client.get_vars */
+	struct llist_head list;
+	/* back-link to ctrl_client */
+	struct ctrl_client *cc;
+	struct {
+		/* CTRL variable name */
+		const char *name;
+		/* display name, if any */
+		const char *display_name;
+	} cfg;
+};
+
+static struct ctrl_client *ctrl_client_find(struct osysmon_state *os, const char *name)
+{
+	struct ctrl_client *cc;
+	llist_for_each_entry(cc, &os->ctrl_clients, list) {
+		if (!strcmp(name, cc->cfg.name))
+			return cc;
+	}
+	return NULL;
+}
+
+static struct ctrl_client *ctrl_client_create(struct osysmon_state *os, const char *name,
+					      const char *host, uint16_t port)
+{
+	struct ctrl_client *cc;
+
+	if (ctrl_client_find(os, name))
+		return NULL;
+
+	cc = talloc_zero(os, struct ctrl_client);
+	if (!cc)
+		return NULL;
+	cc->cfg.name = talloc_strdup(cc, name);
+	cc->cfg.remote_host = talloc_strdup(cc, host);
+	cc->cfg.remote_port = port;
+	INIT_LLIST_HEAD(&cc->get_vars);
+	llist_add_tail(&cc->list, &os->ctrl_clients);
+	/* FIXME */
+	return cc;
+}
+
+static void ctrl_client_destroy(struct ctrl_client *cc)
+{
+	/* FIXME */
+	llist_del(&cc->list);
+	talloc_free(cc);
+}
+
+static struct ctrl_client_get_var *
+ctrl_client_get_var_find_or_create(struct ctrl_client *cc, const char *name)
+{
+	struct ctrl_client_get_var *gv;
+	llist_for_each_entry(gv, &cc->get_vars, list) {
+		if (!strcmp(name, gv->cfg.name))
+			return gv;
+	}
+	gv = talloc_zero(cc, struct ctrl_client_get_var);
+	if (!gv)
+		return NULL;
+	gv->cc = cc;
+	gv->cfg.name = talloc_strdup(gv, name);
+	llist_add_tail(&gv->list, &cc->get_vars);
+	return gv;
+}
+
+/***********************************************************************
+ * VTY
+ ***********************************************************************/
+
+static struct cmd_node ctrl_client_node = {
+	CTRL_CLIENT_NODE,
+	"%s(config-ctrlclient)# ",
+	1,
+};
+
+static struct cmd_node ctrl_client_getvar_node = {
+	CTRL_CLIENT_GETVAR_NODE,
+	"%s(config-ctrlclient-getvar)# ",
+	1,
+};
+
+int osysmon_ctrl_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case CTRL_CLIENT_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		break;
+	case CTRL_CLIENT_GETVAR_NODE:
+		vty->node = CTRL_CLIENT_NODE;
+		{
+			struct ctrl_client_get_var *gv = vty->index;
+			vty->index = gv->cc;
+		}
+		break;
+	default:
+		break;
+	}
+	return vty->node;
+}
+
+
+DEFUN(cfg_ctrl_client, cfg_ctrl_client_cmd,
+	"ctrl-client NAME A.B.C.D <1-65535>",
+	"")
+{
+	struct ctrl_client *cc;
+	cc = ctrl_client_find(g_oss, argv[0]);
+	if (cc) {
+		if ((strcmp(cc->cfg.remote_host, argv[1])) ||
+		    (cc->cfg.remote_port != atoi(argv[2]))) {
+			vty_out(vty, "Client %s has different IP/port, please remove it first%s",
+				cc->cfg.name, VTY_NEWLINE);
+			return CMD_WARNING;
+		}
+	} else
+		cc = ctrl_client_create(g_oss, argv[0], argv[1], atoi(argv[2]));
+	OSMO_ASSERT(cc);
+
+	vty->node = CTRL_CLIENT_NODE;
+	vty->index = cc;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_ctrl_client, cfg_no_ctrl_client_cmd,
+	"no ctrl-client NAME",
+	NO_STR "")
+{
+	struct ctrl_client *cc;
+	cc = ctrl_client_find(g_oss, argv[0]);
+	if (!cc) {
+		vty_out(vty, "Client %s doesn't exist%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	ctrl_client_destroy(cc);
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ctrlc_get_var, cfg_ctrlc_get_var_cmd,
+	"get-variable NAME",
+	"")
+{
+	struct ctrl_client *cc = vty->index;
+	struct ctrl_client_get_var *ccgv;
+
+	ccgv = ctrl_client_get_var_find_or_create(cc, argv[0]);
+	OSMO_ASSERT(ccgv);
+
+	vty->node = CTRL_CLIENT_GETVAR_NODE;
+	vty->index = ccgv;
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_ctrlc_no_get_var, cfg_ctrlc_no_get_var_cmd,
+	"no get-variable NAME",
+	NO_STR "")
+{
+	struct ctrl_client *cc = vty->index;
+	struct ctrl_client_get_var *ccgv;
+
+	ccgv = ctrl_client_get_var_find_or_create(cc, argv[0]);
+	talloc_free(ccgv);
+	return CMD_SUCCESS;
+}
+
+static void write_one_ctrl_client(struct vty *vty, struct ctrl_client *cc)
+{
+	struct ctrl_client_get_var *ccgv;
+	vty_out(vty, "ctrl-client %s %s %u%s", cc->cfg.name,
+		cc->cfg.remote_host, cc->cfg.remote_port, VTY_NEWLINE);
+	llist_for_each_entry(ccgv, &cc->get_vars, list) {
+		vty_out(vty, " get-variable %s%s", ccgv->cfg.name, VTY_NEWLINE);
+		if (ccgv->cfg.display_name)
+			vty_out(vty, " display-name %s%s", ccgv->cfg.display_name, VTY_NEWLINE);
+	}
+}
+
+static int config_write_ctrl_client(struct vty *vty)
+{
+	struct ctrl_client *cc;
+
+	llist_for_each_entry(cc, &g_oss->ctrl_clients, list)
+		write_one_ctrl_client(vty, cc);
+	return CMD_SUCCESS;
+}
+
+static void osysmon_ctrl_vty_init(void)
+{
+	install_element(CONFIG_NODE, &cfg_ctrl_client_cmd);
+	install_element(CONFIG_NODE, &cfg_no_ctrl_client_cmd);
+	install_node(&ctrl_client_node, config_write_ctrl_client);
+	install_element(CTRL_CLIENT_NODE, &cfg_ctrlc_get_var_cmd);
+	install_element(CTRL_CLIENT_NODE, &cfg_ctrlc_no_get_var_cmd);
+	install_node(&ctrl_client_getvar_node, NULL);
+	//install_element(CTRL_CLIENT_GETVAR_NODE, &cfg_getvar_disp_name_cmd);
+}
+
+
+/***********************************************************************
+ * Runtime Code
+ ***********************************************************************/
+
+/* called once on startup before config file parsing */
+int osysmon_ctrl_init()
+{
+	osysmon_ctrl_vty_init();
+	return 0;
+}
+
+static int ctrl_client_poll(struct ctrl_client *cc, struct value_node *parent)
+{
+	struct ctrl_client_get_var *ccgv;
+	struct value_node *vn_clnt = value_node_add(parent, parent, cc->cfg.name, NULL);
+
+	/* attempt to re-connect */
+	if (!cc->sch)
+		cc->sch = simple_ctrl_open(cc, cc->cfg.remote_host, cc->cfg.remote_port, 1000);
+	/* abort, if that failed */
+	if (!cc->sch) {
+		return -1;
+	}
+
+	llist_for_each_entry(ccgv, &cc->get_vars, list) {
+		char *value = simple_ctrl_get(cc->sch, ccgv->cfg.name);
+
+		/* FIXME: Distinguish between ERROR reply and
+		 * connection issues */
+		/* Close connection on error */
+		if (!value) {
+			simple_ctrl_close(cc->sch);
+			cc->sch = NULL;
+			return 0;
+		}
+
+		value_node_add(vn_clnt, vn_clnt, ccgv->cfg.name, value);
+		free(value); /* no talloc, this is from sscanf() */
+	}
+	return 0;
+}
+
+/* called periodically */
+int osysmon_ctrl_poll(struct value_node *parent)
+{
+	struct ctrl_client *cc;
+	llist_for_each_entry(cc, &g_oss->ctrl_clients, list)
+		ctrl_client_poll(cc, parent);
+	return 0;
+}
diff --git a/src/osysmon_file.c b/src/osysmon_file.c
new file mode 100644
index 0000000..3a228fa
--- /dev/null
+++ b/src/osysmon_file.c
@@ -0,0 +1,162 @@
+/* Simple Osmocom System Monitor (osysmon): Support for monitoring files */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <string.h>
+#include <sys/sysinfo.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+#include "osysmon.h"
+#include "value_node.h"
+
+/***********************************************************************
+ * Data model
+ ***********************************************************************/
+
+struct osysmon_file {
+	struct llist_head list;
+	struct {
+		const char *name;
+		const char *path;
+	} cfg;
+};
+
+static struct osysmon_file *osysmon_file_find(const char *name)
+{
+	struct osysmon_file *of;
+
+	llist_for_each_entry(of, &g_oss->files, list) {
+		if (!strcmp(of->cfg.name, name))
+			return of;
+	}
+	return NULL;
+}
+
+static struct osysmon_file *osysmon_file_add(const char *name, const char *path)
+{
+	struct osysmon_file *of;
+
+	if (osysmon_file_find(name))
+		return NULL;
+
+	of = talloc_zero(g_oss, struct osysmon_file);
+	OSMO_ASSERT(of);
+	of->cfg.name = talloc_strdup(of, name);
+	of->cfg.path = talloc_strdup(of, path);
+	llist_add_tail(&of->list, &g_oss->files);
+	return of;
+}
+
+static void osysmon_file_destroy(struct osysmon_file *of)
+{
+	llist_del(&of->list);
+	talloc_free(of);
+}
+
+static void osysmon_file_read(struct osysmon_file *of, struct value_node *parent)
+{
+	char buf[512];
+	char *s, *nl;
+	FILE *f;
+
+	f = fopen(of->cfg.path, "r");
+	if (!f) {
+		value_node_add(parent, parent, of->cfg.name, "<NOTFOUND>");
+		return;
+	}
+	s = fgets(buf, sizeof(buf), f);
+	fclose(f);
+	if (s == NULL) {
+		value_node_add(parent, parent, of->cfg.name, "<EMPTY>");
+		return;
+	}
+	buf[sizeof(buf)-1] = '\0';
+	while ((nl = strrchr(buf, '\n')))
+		*nl = '\0';
+	value_node_add(parent, parent, of->cfg.name, buf);
+}
+
+/***********************************************************************
+ * VTY
+ ***********************************************************************/
+
+#define FILE_STR "Configure a file to be monitored/watched\n"
+DEFUN(cfg_file, cfg_file_cmd,
+	"file NAME PATH",
+	FILE_STR "Name of this file-watcher\n" "Path of file in filesystem\n")
+{
+	struct osysmon_file *of;
+	of = osysmon_file_add(argv[0], argv[1]);
+	if (!of) {
+		vty_out(vty, "Couldn't add file-watcher, maybe it exists?%s", VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_file, cfg_no_file_cmd,
+	"no file NAME",
+	NO_STR FILE_STR "Name of this file-watcher\n")
+{
+	struct osysmon_file *of;
+	of = osysmon_file_find(argv[0]);
+	if (!of) {
+		vty_out(vty, "Cannot find file-watcher for '%s'%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	osysmon_file_destroy(of);
+	return CMD_SUCCESS;
+}
+
+
+static void osysmon_file_vty_init(void)
+{
+	install_element(CONFIG_NODE, &cfg_file_cmd);
+	install_element(CONFIG_NODE, &cfg_no_file_cmd);
+}
+
+/***********************************************************************
+ * Runtime Code
+ ***********************************************************************/
+
+/* called once on startup before config file parsing */
+int osysmon_file_init()
+{
+	osysmon_file_vty_init();
+	return 0;
+}
+
+/* called periodically */
+int osysmon_file_poll(struct value_node *parent)
+{
+	struct value_node *vn_file;
+	struct osysmon_file *of;
+
+	vn_file = value_node_add(parent, parent, "file", NULL);
+
+	llist_for_each_entry(of, &g_oss->files, list)
+		osysmon_file_read(of, vn_file);
+
+	return 0;
+}
diff --git a/src/osysmon_main.c b/src/osysmon_main.c
new file mode 100644
index 0000000..5983212
--- /dev/null
+++ b/src/osysmon_main.c
@@ -0,0 +1,153 @@
+/* Simple Osmocom System Monitor (osysmon) */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <signal.h>
+
+#include "config.h"
+#include "osysmon.h"
+#include "value_node.h"
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/logging.h>
+#include <osmocom/core/application.h>
+
+static struct log_info log_info = {};
+
+static int osysmon_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case CTRL_CLIENT_NODE:
+	case CTRL_CLIENT_GETVAR_NODE:
+		return osysmon_ctrl_go_parent(vty);
+	}
+	return vty->node;
+}
+
+static int osysmon_is_config_node(struct vty *vty, int node)
+{
+	switch (node) {
+	/* no non-config-nodes */
+	default:
+		return 1;
+	}
+}
+
+
+static struct vty_app_info vty_info = {
+	.name = "osysmon",
+	.copyright =
+	"Copyright (C) 2008-2018 Harald Welte\r\n"
+	"License GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl-2.0.html>\r\n"
+	"This is free software: you are free to change and redistribute it.\r\n"
+	"There is NO WARRANTY, to the extent permitted by law.\r\n",
+	.version = PACKAGE_VERSION,
+	.go_parent_cb = osysmon_go_parent,
+	.is_config_node = osysmon_is_config_node,
+};
+
+
+static const char *config_file = "osmo-sysmon.cfg";
+struct osysmon_state *g_oss;
+
+
+static void print_node(struct value_node *node, unsigned int indent)
+{
+	unsigned int i;
+
+	if (node->value) {
+		for (i = 0; i < indent; i++)
+			fputc(' ', stdout);
+		printf("%s: %s\n", node->name, node->value);
+	} else {
+		struct value_node *vn;
+		for (i = 0; i < indent; i++)
+			fputc(' ', stdout);
+		printf("%s\n", node->name);
+		llist_for_each_entry(vn, &node->children, list)
+			print_node(vn, indent+2);
+	}
+}
+
+static void display_update(struct value_node *root)
+{
+	print_node(root, 0);
+}
+
+static void signal_handler(int signal)
+{
+	fprintf(stderr, "Signal %u received", signal);
+
+	switch(signal) {
+	case SIGUSR1:
+		talloc_report(g_oss, stderr);
+		break;
+	default:
+		break;
+	}
+}
+
+int main(int argc, char **argv)
+{
+	int rc;
+
+	osmo_init_logging2(NULL, &log_info);
+
+	g_oss = talloc_zero(NULL, struct osysmon_state);
+	INIT_LLIST_HEAD(&g_oss->ctrl_clients);
+	INIT_LLIST_HEAD(&g_oss->netdevs);
+	INIT_LLIST_HEAD(&g_oss->files);
+
+	vty_init(&vty_info);
+	osysmon_sysinfo_init();
+	osysmon_ctrl_init();
+	osysmon_rtnl_init();
+	osysmon_file_init();
+
+	rc = vty_read_config_file(config_file, NULL);
+	if (rc < 0) {
+		fprintf(stderr, "Failed to parse the config file %s\n", config_file);
+		exit(2);
+	}
+
+	signal(SIGUSR1, &signal_handler);
+	signal(SIGUSR2, &signal_handler);
+	osmo_init_ignore_signals();
+
+	while (1) {
+		struct value_node *root = value_node_add(g_oss, NULL, "root", NULL);
+		osysmon_sysinfo_poll(root);
+		osysmon_ctrl_poll(root);
+		osysmon_rtnl_poll(root);
+		osysmon_file_poll(root);
+
+		display_update(root);
+		value_node_del(root);
+		sleep(1);
+	}
+
+	exit(0);
+}
diff --git a/src/osysmon_rtnl.c b/src/osysmon_rtnl.c
new file mode 100644
index 0000000..94d8bf9
--- /dev/null
+++ b/src/osysmon_rtnl.c
@@ -0,0 +1,416 @@
+/* Simple Osmocom System Monitor (osysmon): Support for monitoring net-devices */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <time.h>
+#include <arpa/inet.h>
+
+#include <linux/if.h>
+#include <linux/if_link.h>
+#include <linux/rtnetlink.h>
+
+#include <libmnl/libmnl.h>
+#include <talloc.h>
+
+#include "value_node.h"
+#include "osysmon.h"
+
+struct rtnl_client_state {
+	struct mnl_socket *nl;
+};
+
+
+struct netdev {
+	struct llist_head list;
+	struct {
+		const char *name;
+	} cfg;
+};
+
+static struct netdev *netdev_find(struct osysmon_state *os, const char *name)
+{
+	struct netdev *nd;
+	llist_for_each_entry(nd, &os->netdevs, list) {
+		if (!strcmp(name, nd->cfg.name))
+			return nd;
+	}
+	return NULL;
+}
+
+static struct netdev *netdev_create(struct osysmon_state *os, const char *name)
+{
+	struct netdev *nd;
+
+	if (netdev_find(os, name))
+		return NULL;
+
+	nd = talloc_zero(os, struct netdev);
+	if (!nd)
+		return NULL;
+	nd->cfg.name = talloc_strdup(os, name);
+	llist_add_tail(&nd->list, &os->netdevs);
+	return nd;
+}
+
+static void netdev_destroy(struct netdev *nd)
+{
+	llist_del(&nd->list);
+	talloc_free(nd);
+}
+
+/***********************************************************************
+ * VTY
+ ***********************************************************************/
+
+static struct cmd_node netdev_node = {
+	NETDEV_NODE,
+	"%s(config-netdev)# ",
+	1,
+};
+
+int osysmon_netdev_go_parent(struct vty *vty)
+{
+	switch (vty->node) {
+	case NETDEV_NODE:
+		vty->node = CONFIG_NODE;
+		vty->index = NULL;
+		break;
+	default:
+		break;
+	}
+	return vty->node;
+}
+
+DEFUN(cfg_netdev, cfg_netdev_cmd,
+	"netdev NAME",
+	"")
+{
+	struct netdev *nd;
+	nd = netdev_find(g_oss, argv[0]);
+	if (!nd)
+		nd = netdev_create(g_oss, argv[0]);
+	OSMO_ASSERT(nd);
+
+	return CMD_SUCCESS;
+}
+
+DEFUN(cfg_no_netdev, cfg_no_netdev_cmd,
+	"no netdev NAME",
+	"")
+{
+	struct netdev *nd;
+	nd = netdev_find(g_oss, argv[0]);
+	if (!nd) {
+		vty_out(vty, "Netdev %s doesn't exist in configuration%s", argv[0], VTY_NEWLINE);
+		return CMD_WARNING;
+	}
+	netdev_destroy(nd);
+	return CMD_SUCCESS;
+}
+
+static void write_one_netdev(struct vty *vty, struct netdev *nd)
+{
+	vty_out(vty, "netdev %s%s", nd->cfg.name, VTY_NEWLINE);
+}
+
+static int config_write_netdev(struct vty *vty)
+{
+	struct netdev *nd;
+
+	llist_for_each_entry(nd, &g_oss->netdevs, list)
+		write_one_netdev(vty, nd);
+	return CMD_SUCCESS;
+}
+
+static void osysmon_rtnl_vty_init(void)
+{
+	install_element(CONFIG_NODE, &cfg_netdev_cmd);
+	install_element(CONFIG_NODE, &cfg_no_netdev_cmd);
+	install_node(&netdev_node, config_write_netdev);
+}
+
+
+/***********************************************************************
+ * Interface Level
+ ***********************************************************************/
+
+static int if_attr_cb(const struct nlattr *attr, void *data)
+{
+	const struct nlattr **tb = data;
+	int type = mnl_attr_get_type(attr);
+
+	/* skip unsupported attribute in user-space */
+	if (mnl_attr_type_valid(attr, IFLA_MAX) < 0)
+		return MNL_CB_OK;
+
+	switch(type) {
+	case IFLA_ADDRESS:
+		if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0) {
+			perror("mnl_attr_validate");
+			return MNL_CB_ERROR;
+		}
+		break;
+	case IFLA_MTU:
+		if (mnl_attr_validate(attr, MNL_TYPE_U32) < 0) {
+			perror("mnl_attr_validate");
+			return MNL_CB_ERROR;
+		}
+		break;
+	case IFLA_IFNAME:
+		if (mnl_attr_validate(attr, MNL_TYPE_STRING) < 0) {
+			perror("mnl_attr_validate");
+			return MNL_CB_ERROR;
+		}
+		break;
+	}
+	tb[type] = attr;
+	return MNL_CB_OK;
+}
+
+static int data_cb(const struct nlmsghdr *nlh, void *data)
+{
+	struct ifinfomsg *ifm = mnl_nlmsg_get_payload(nlh);
+	struct value_node *parent = data;
+	struct value_node *vn_if;
+	const char *name;
+	char buf[32];
+
+	struct nlattr *tb[IFLA_MAX+1] = {};
+	mnl_attr_parse(nlh, sizeof(*ifm), if_attr_cb, tb);
+	if (!tb[IFLA_IFNAME])
+		return MNL_CB_OK;
+	name = mnl_attr_get_str(tb[IFLA_IFNAME]);
+
+	/* skip any non-configured interface names */
+	if (!netdev_find(g_oss, name))
+		return MNL_CB_OK;
+
+	vn_if = value_node_find_or_add(parent, talloc_strdup(parent, name));
+	OSMO_ASSERT(vn_if);
+	vn_if->idx = ifm->ifi_index;
+
+	if (tb[IFLA_ADDRESS] && mnl_attr_get_payload_len(tb[IFLA_ADDRESS]) == 6) {
+		uint8_t *hwaddr = mnl_attr_get_payload(tb[IFLA_ADDRESS]);
+		snprintf(buf, sizeof(buf), "%02x:%02x:%02x:%02x:%02x:%02x",
+			 hwaddr[0], hwaddr[1], hwaddr[2], hwaddr[3], hwaddr[4], hwaddr[5]);
+		value_node_add(vn_if, vn_if, "hwaddr", buf);
+	}
+	if (ifm->ifi_flags & IFF_RUNNING) 
+		value_node_add(vn_if, vn_if, "running", "true");
+	else
+		value_node_add(vn_if, vn_if, "running", "false");
+
+	if (ifm->ifi_flags & IFF_UP)
+		value_node_add(vn_if, vn_if, "up", "true");
+	else
+		value_node_add(vn_if, vn_if, "up", "false");
+
+	return MNL_CB_OK;
+}
+
+static int rtnl_update_link(struct rtnl_client_state *rcs, struct value_node *parent)
+{
+	char buf[MNL_SOCKET_BUFFER_SIZE];
+	struct nlmsghdr *nlh;
+	struct rtgenmsg *rt;
+	int ret;
+	unsigned int seq, portid;
+
+	nlh = mnl_nlmsg_put_header(buf);
+	nlh->nlmsg_type	= RTM_GETLINK;
+	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+	nlh->nlmsg_seq = seq = time(NULL);
+	rt = mnl_nlmsg_put_extra_header(nlh, sizeof(struct rtgenmsg));
+	rt->rtgen_family = AF_PACKET;
+
+	portid = mnl_socket_get_portid(rcs->nl);
+
+	if (mnl_socket_sendto(rcs->nl, nlh, nlh->nlmsg_len) < 0) {
+		perror("mnl_socket_sendto");
+		exit(EXIT_FAILURE);
+	}
+
+	ret = mnl_socket_recvfrom(rcs->nl, buf, sizeof(buf));
+	while (ret > 0) {
+		ret = mnl_cb_run(buf, ret, seq, portid, data_cb, parent);
+		if (ret <= MNL_CB_STOP)
+			break;
+		ret = mnl_socket_recvfrom(rcs->nl, buf, sizeof(buf));
+	}
+	if (ret == -1) {
+		perror("error");
+		return -1;
+	}
+	return 0;
+}
+
+
+
+/***********************************************************************
+ * L3 Address Level
+ ***********************************************************************/
+
+static int inet_attr_cb(const struct nlattr *attr, void *data)
+{
+	const struct nlattr **tb = data;
+	int type = mnl_attr_get_type(attr);
+
+	/* skip unsupported attribute in user-space */
+	if (mnl_attr_type_valid(attr, IFA_MAX) < 0)
+		return MNL_CB_OK;
+
+	switch(type) {
+	case IFA_ADDRESS:
+		if (mnl_attr_validate(attr, MNL_TYPE_BINARY) < 0) {
+			perror("mnl_attr_validate");
+			return MNL_CB_ERROR;
+		}
+		break;
+	}
+	tb[type] = attr;
+	return MNL_CB_OK;
+}
+
+static int inet_data_cb(const struct nlmsghdr *nlh, void *data)
+{
+	struct ifaddrmsg *ifa = mnl_nlmsg_get_payload(nlh);
+	struct nlattr *tb[IFA_MAX + 1] = {};
+	struct value_node *parent = data;
+	struct value_node *vn_if;
+
+	vn_if = value_node_find_by_idx(parent, ifa->ifa_index);
+	if (!vn_if)
+		return MNL_CB_OK;
+
+	if (ifa->ifa_family != AF_INET)
+		return MNL_CB_OK;
+
+	mnl_attr_parse(nlh, sizeof(*ifa), inet_attr_cb, tb);
+	if (tb[IFA_ADDRESS]) {
+		struct in_addr *addr = mnl_attr_get_payload(tb[IFA_ADDRESS]);
+		char out[INET_ADDRSTRLEN+32];
+		snprintf(out, sizeof(out), "%s/%u", inet_ntoa(*addr), ifa->ifa_prefixlen);
+		value_node_add(vn_if, vn_if, "ip", out);
+	}
+
+	return MNL_CB_OK;
+}
+
+static int rtnl_update_addr(struct rtnl_client_state *rcs, struct value_node *parent)
+{
+	char buf[MNL_SOCKET_BUFFER_SIZE];
+	struct nlmsghdr *nlh;
+	struct rtgenmsg *rt;
+	int ret;
+	unsigned int seq, portid;
+
+	nlh = mnl_nlmsg_put_header(buf);
+	nlh->nlmsg_type	= RTM_GETADDR;
+	nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+	nlh->nlmsg_seq = seq = time(NULL);
+	rt = mnl_nlmsg_put_extra_header(nlh, sizeof(struct rtgenmsg));
+	rt->rtgen_family = AF_INET;
+
+	portid = mnl_socket_get_portid(rcs->nl);
+
+	if (mnl_socket_sendto(rcs->nl, nlh, nlh->nlmsg_len) < 0) {
+		perror("mnl_socket_sendto");
+		exit(EXIT_FAILURE);
+	}
+
+	ret = mnl_socket_recvfrom(rcs->nl, buf, sizeof(buf));
+	while (ret > 0) {
+		ret = mnl_cb_run(buf, ret, seq, portid, inet_data_cb, parent);
+		if (ret <= MNL_CB_STOP)
+			break;
+		ret = mnl_socket_recvfrom(rcs->nl, buf, sizeof(buf));
+	}
+	if (ret == -1) {
+		perror("error");
+		return -1;
+	}
+
+	return 0;
+}
+
+
+
+/***********************************************************************
+ * Common Code / API
+ ***********************************************************************/
+
+int osysmon_rtnl_init()
+{
+	osysmon_rtnl_vty_init();
+	return 0;
+}
+
+
+
+struct rtnl_client_state *rtnl_init(void *ctx)
+{
+	struct mnl_socket *nl;
+	struct rtnl_client_state *rcs;
+
+	nl = mnl_socket_open(NETLINK_ROUTE);
+	if (nl == NULL) {
+		perror("mnl_socket_open");
+		return NULL;
+	}
+	if (mnl_socket_bind(nl, 0, MNL_SOCKET_AUTOPID) < 0) {
+		perror("mnl_socket_bind");
+		mnl_socket_close(nl);
+		return NULL;
+	}
+
+	rcs = talloc_zero(ctx, struct rtnl_client_state);
+	if (!rcs) {
+		mnl_socket_close(nl);
+		return NULL;
+	}
+
+	rcs->nl = nl;
+
+	return rcs;
+}
+
+int osysmon_rtnl_poll(struct value_node *parent)
+{
+	struct value_node *vn_net;
+
+	if (!g_oss->rcs)
+		g_oss->rcs = rtnl_init(NULL);
+
+	vn_net = value_node_add(parent, parent, "netdev", NULL);
+
+	if (!g_oss->rcs)
+		return -1;
+
+	rtnl_update_link(g_oss->rcs, vn_net);
+	rtnl_update_addr(g_oss->rcs, vn_net);
+
+	return 0;
+}
diff --git a/src/osysmon_sysinfo.c b/src/osysmon_sysinfo.c
new file mode 100644
index 0000000..50616bc
--- /dev/null
+++ b/src/osysmon_sysinfo.c
@@ -0,0 +1,88 @@
+/* Simple Osmocom System Monitor (osysmon): Support for uptime/load/ram */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <string.h>
+#include <sys/sysinfo.h>
+
+#include <osmocom/vty/vty.h>
+#include <osmocom/vty/command.h>
+
+#include "osysmon.h"
+#include "value_node.h"
+
+/***********************************************************************
+ * Runtime Code
+ ***********************************************************************/
+
+/* called once on startup before config file parsing */
+int osysmon_sysinfo_init()
+{
+	return 0;
+}
+
+static float loadfac(unsigned long in) {
+	return in/65536.0;
+}
+
+#define to_mbytes(in) ((in)/((1024*1024)/si.mem_unit))
+
+#define SECS_PER_MIN	60UL
+#define MINS_PER_HOUR	60UL
+#define HOURS_PER_DAY	24UL
+
+#define to_days(in)	((in)/(SECS_PER_MIN*MINS_PER_HOUR*HOURS_PER_DAY))
+#define to_hours(in)	(((in)/(SECS_PER_MIN*MINS_PER_HOUR))%HOURS_PER_DAY)
+#define to_minutes(in)	(((in)/(SECS_PER_MIN))%MINS_PER_HOUR)
+#define to_seconds(in)	((in)%SECS_PER_MIN)
+
+/* called periodically */
+int osysmon_sysinfo_poll(struct value_node *parent)
+{
+	struct sysinfo si;
+	struct value_node *vn_sysinfo;
+	char buf[32];
+	int rc;
+
+	vn_sysinfo = value_node_add(parent, parent, "sysinfo", NULL);
+
+	rc = sysinfo(&si);
+	if (rc < 0)
+		return rc;
+
+	/* Load Factor 1/5/15min */
+	snprintf(buf, sizeof(buf), "%.2f/%.2f/%.2f",
+		 loadfac(si.loads[0]), loadfac(si.loads[1]), loadfac(si.loads[2]));
+	value_node_add(vn_sysinfo, vn_sysinfo, "load", buf);
+
+	/* RAM information (total/free/sared) in megabytes */
+	snprintf(buf, sizeof(buf), "%lu/%lu/%lu",
+		to_mbytes(si.totalram), to_mbytes(si.freeram), to_mbytes(si.sharedram));
+	value_node_add(vn_sysinfo, vn_sysinfo, "ram", buf);
+
+	/* uptime in days/hours/minutes/seconds */
+	snprintf(buf, sizeof(buf), "%lud %02lu:%02lu:%02lu", to_days(si.uptime),
+		 to_hours(si.uptime), to_minutes(si.uptime), to_seconds(si.uptime));
+	value_node_add(vn_sysinfo, vn_sysinfo, "uptime", buf);
+
+	return 0;
+}
diff --git a/src/simple_ctrl.c b/src/simple_ctrl.c
new file mode 100644
index 0000000..2261323
--- /dev/null
+++ b/src/simple_ctrl.c
@@ -0,0 +1,329 @@
+/* Simple, blocking client API against the Osmocom CTRL interface */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <unistd.h>
+#include <stdint.h>
+#include <talloc.h>
+#include <string.h>
+#include <errno.h>
+#include <sys/ioctl.h>
+
+#include <netinet/in.h>
+
+#include <osmocom/core/msgb.h>
+#include <osmocom/core/socket.h>
+#include <osmocom/gsm/ipa.h>
+#include <osmocom/gsm/protocol/ipaccess.h>
+
+#include "simple_ctrl.h"
+
+/***********************************************************************
+ * blocking I/O with timeout helpers
+ ***********************************************************************/
+
+static struct timeval *timeval_from_msec(uint32_t tout_msec)
+{
+	static struct timeval tout;
+
+	if (tout_msec == 0)
+		return NULL;
+	tout.tv_sec = tout_msec/1000;
+	tout.tv_usec = (tout_msec%1000)*1000;
+
+	return &tout;
+}
+
+static ssize_t read_timeout(int fd, void *buf, size_t count, uint32_t tout_msec)
+{
+	fd_set readset;
+	int rc;
+
+	FD_ZERO(&readset);
+	FD_SET(fd, &readset);
+
+	rc = select(fd+1, &readset, NULL, NULL, timeval_from_msec(tout_msec));
+	if (rc < 0)
+		return rc;
+
+	if (FD_ISSET(fd, &readset))
+		return read(fd, buf, count);
+
+	return -ETIMEDOUT;
+}
+
+static ssize_t write_timeout(int fd, const void *buf, size_t count, uint32_t tout_msec)
+{
+	fd_set writeset;
+	int rc;
+
+	FD_ZERO(&writeset);
+	FD_SET(fd, &writeset);
+
+	rc = select(fd+1, NULL, &writeset, NULL, timeval_from_msec(tout_msec));
+	if (rc < 0)
+		return rc;
+
+	if (FD_ISSET(fd, &writeset))
+		return write(fd, buf, count);
+
+	return -ETIMEDOUT;
+}
+
+
+/***********************************************************************
+ * actual CTRL client API
+ ***********************************************************************/
+
+struct simple_ctrl_handle {
+	int fd;
+	uint32_t next_id;
+	uint32_t tout_msec;
+};
+
+struct simple_ctrl_handle *simple_ctrl_open(void *ctx, const char *host, uint16_t dport,
+					    uint32_t tout_msec)
+{
+	struct simple_ctrl_handle *sch;
+	fd_set writeset;
+	int off = 0;
+	int rc, fd;
+
+	fd = osmo_sock_init(AF_INET, SOCK_STREAM, IPPROTO_TCP, host, dport,
+			    OSMO_SOCK_F_CONNECT | OSMO_SOCK_F_NONBLOCK);
+	if (fd < 0) {
+		fprintf(stderr, "CTRL: error connecting socket: %s\n", strerror(errno));
+		return NULL;
+	}
+
+	/* wait until connect (or timeout) happens */
+	FD_ZERO(&writeset);
+	FD_SET(fd, &writeset);
+	rc = select(fd+1, NULL, &writeset, NULL, timeval_from_msec(tout_msec));
+	if (rc == 0) {
+		fprintf(stderr, "CTRL: timeout during connect\n");
+		goto out_close;
+	}
+	if (rc < 0) {
+		fprintf(stderr, "CTRL: error connecting socket: %s\n", strerror(errno));
+		goto out_close;
+	}
+
+	/* set FD blocking again */
+	if (ioctl(fd, FIONBIO, (unsigned char *)&off) < 0) {
+		fprintf(stderr, "CTRL: cannot set socket blocking: %s\n", strerror(errno));
+		goto out_close;
+	}
+
+	sch = talloc_zero(ctx, struct simple_ctrl_handle);
+	if (!sch)
+		goto out_close;
+	sch->fd = fd;
+	sch->tout_msec = tout_msec;
+	return sch;
+
+out_close:
+	close(fd);
+	return NULL;
+}
+
+void simple_ctrl_set_timeout(struct simple_ctrl_handle *sch, uint32_t tout_msec)
+{
+	sch->tout_msec = tout_msec;
+}
+
+void simple_ctrl_close(struct simple_ctrl_handle *sch)
+{
+	close(sch->fd);
+	talloc_free(sch);
+}
+
+static struct msgb *simple_ipa_receive(struct simple_ctrl_handle *sch)
+{
+	struct ipaccess_head hh;
+	struct msgb *resp;
+	int rc, len;
+
+	rc = read_timeout(sch->fd, (uint8_t *) &hh, sizeof(hh), sch->tout_msec);
+	if (rc < 0) {
+		fprintf(stderr, "CTRL: Error during read: %d\n", rc);
+		return NULL;
+	} else if (rc < sizeof(hh)) {
+		fprintf(stderr, "CTRL: ERROR: short read (header)\n");
+		return NULL;
+	}
+	len = ntohs(hh.len);
+
+	resp = msgb_alloc(len+sizeof(hh)+1, "CTRL Rx");
+	if (!resp)
+		return NULL;
+	resp->l1h = msgb_put(resp, sizeof(hh));
+	memcpy(resp->l1h, (uint8_t *) &hh, sizeof(hh));
+
+	resp->l2h = resp->tail;
+	rc = read(sch->fd, resp->l2h, len);
+	if (rc < len) {
+		fprintf(stderr, "CTRL: ERROR: short read (payload)\n");
+		msgb_free(resp);
+		return NULL;
+	}
+	msgb_put(resp, rc);
+
+	return resp;
+}
+
+struct msgb *simple_ctrl_receive(struct simple_ctrl_handle *sch)
+{
+	struct msgb *resp;
+	struct ipaccess_head *ih;
+	struct ipaccess_head_ext *ihe;
+	unsigned char *tmp;
+
+	/* loop until we've received a CTRL message */
+	while (true) {
+		resp = simple_ipa_receive(sch);
+		if (!resp)
+			return NULL;
+
+		ih = (struct ipaccess_head *) resp->l1h;
+		if (ih->proto == IPAC_PROTO_OSMO)
+			resp->l2h = resp->l2h+1;
+		ihe = (struct ipaccess_head_ext*) (resp->l1h + sizeof(*ih));
+		if (ih->proto == IPAC_PROTO_OSMO && ihe->proto == IPAC_PROTO_EXT_CTRL) {
+			/* Ensure data is NULL terminated */
+			tmp = msgb_put(resp, 1);
+			*tmp = '\0';
+			return resp;
+		} else {
+			fprintf(stderr, "unknown IPA message %s\n", msgb_hexdump(resp));
+			msgb_free(resp);
+		}
+	}
+}
+
+static int simple_ctrl_send(struct simple_ctrl_handle *sch, struct msgb *msg)
+{
+	int rc;
+
+	ipa_prepend_header_ext(msg, IPAC_PROTO_EXT_CTRL);
+	ipa_prepend_header(msg, IPAC_PROTO_OSMO);
+
+	rc = write_timeout(sch->fd, msg->data, msg->len, sch->tout_msec);
+	if (rc < 0) {
+		fprintf(stderr, "CTRL: Error during write: %d\n", rc);
+		return rc;
+	} else if (rc < msg->len) {
+		fprintf(stderr, "CTRL: ERROR: short write\n");
+		msgb_free(msg);
+		return -1;
+	} else {
+		msgb_free(msg);
+		return 0;
+	}
+}
+
+static struct msgb *simple_ctrl_xceive(struct simple_ctrl_handle *sch, struct msgb *msg)
+{
+	int rc;
+
+	rc = simple_ctrl_send(sch, msg);
+	if (rc < 0)
+		return NULL;
+
+	/* FIXME: ignore any TRAP */
+	return simple_ctrl_receive(sch);
+}
+
+char *simple_ctrl_get(struct simple_ctrl_handle *sch, const char *var)
+{
+	struct msgb *msg = msgb_alloc_headroom(512+8, 8, "CTRL GET");
+	struct msgb *resp;
+	unsigned int rx_id;
+	char *rx_var, *rx_val;
+	int rc;
+
+	if (!msg)
+		return NULL;
+
+	rc = msgb_printf(msg, "GET %u %s", sch->next_id++, var);
+	if (rc < 0) {
+		msgb_free(msg);
+		return NULL;
+	}
+	resp = simple_ctrl_xceive(sch, msg);
+	if (!resp)
+		return NULL;
+
+	rc = sscanf(msgb_l2(resp), "GET_REPLY %u %ms %ms", &rx_id, &rx_var, &rx_val);
+	if (rc == 3) {
+		if (rx_id == sch->next_id-1 && !strcmp(var, rx_var)) {
+			free(rx_var);
+			msgb_free(resp);
+			return rx_val;
+		}
+		free(rx_var);
+		free(rx_val);
+	} else {
+		fprintf(stderr, "CTRL: ERROR: GET(%s) results in '%s'\n", var, (char *)msgb_l2(resp));
+	}
+
+	msgb_free(resp);
+	return NULL;
+}
+
+int simple_ctrl_set(struct simple_ctrl_handle *sch, const char *var, const char *val)
+{
+	struct msgb *msg = msgb_alloc_headroom(512+8, 8, "CTRL SET");
+	struct msgb *resp;
+	unsigned int rx_id;
+	char *rx_var, *rx_val;
+	int rc;
+
+	if (!msg)
+		return -1;
+
+	rc = msgb_printf(msg, "SET %u %s %s", sch->next_id++, var, val);
+	if (rc < 0) {
+		msgb_free(msg);
+		return -1;
+	}
+	resp = simple_ctrl_xceive(sch, msg);
+	if (!resp)
+		return -1;
+
+	if (sscanf(msgb_l2(resp), "SET_REPLY %u %ms %ms", &rx_id, &rx_var, &rx_val) == 3) {
+		if (rx_id == sch->next_id-1 && !strcmp(var, rx_var) && !strcmp(val, rx_val)) {
+			free(rx_val);
+			free(rx_var);
+			msgb_free(resp);
+			return 0;
+		} else {
+			free(rx_val);
+			free(rx_var);
+		}
+	} else {
+		fprintf(stderr, "CTRL: ERROR: SET(%s=%s) results in '%s'\n", var, val, (char *) msgb_l2(resp));
+	}
+
+	msgb_free(resp);
+	return -1;
+}
diff --git a/src/simple_ctrl.h b/src/simple_ctrl.h
new file mode 100644
index 0000000..81a759d
--- /dev/null
+++ b/src/simple_ctrl.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdint.h>
+
+struct simple_ctrl_handle;
+
+struct simple_ctrl_handle *simple_ctrl_open(void *ctx, const char *host, uint16_t dport,
+					    uint32_t tout_msec);
+void simple_ctrl_close(struct simple_ctrl_handle *sch);
+
+void simple_ctrl_set_timeout(struct simple_ctrl_handle *sch, uint32_t tout_msec);
+struct msgb *simple_ctrl_receive(struct simple_ctrl_handle *sch);
+
+char *simple_ctrl_get(struct simple_ctrl_handle *sch, const char *var);
+int simple_ctrl_set(struct simple_ctrl_handle *sch, const char *var, const char *val);
+
diff --git a/src/value_node.c b/src/value_node.c
new file mode 100644
index 0000000..38fb5c7
--- /dev/null
+++ b/src/value_node.c
@@ -0,0 +1,98 @@
+/* Simple Osmocom System Monitor (osysmon): Name-Value tree */
+
+/* (C) 2018 by Harald Welte <laforge@gnumonks.org>
+ * All Rights Reserved.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ *  MA  02110-1301, USA.
+ */
+
+#include <talloc.h>
+#include <string.h>
+#include <osmocom/core/utils.h>
+
+#include "value_node.h"
+
+struct value_node *value_node_add(void *ctx, struct value_node *parent,
+				  const char *name, const char *value)
+{
+	struct value_node *vn = talloc_zero(parent, struct value_node);
+
+	if (parent && value_node_find(parent, name)) {
+		/* duplicate name not permitted! */
+		return NULL;
+	}
+
+	vn = talloc_zero(parent, struct value_node);
+	OSMO_ASSERT(vn);
+
+	/* we assume the name is static/const and owned by caller */
+	vn->name = name;
+	if (value)
+		vn->value = talloc_strdup(vn, value);
+	INIT_LLIST_HEAD(&vn->children);
+	if (parent)
+		llist_add_tail(&vn->list, &parent->children);
+	else
+		INIT_LLIST_HEAD(&vn->list);
+	return vn;
+}
+
+struct value_node *value_node_find(struct value_node *parent, const char *name)
+{
+	struct value_node *vn;
+	llist_for_each_entry(vn, &parent->children, list) {
+		if (!strcmp(name, vn->name))
+			return vn;
+	}
+	return NULL;
+}
+
+struct value_node *value_node_find_or_add(struct value_node *parent, const char *name)
+{
+	struct value_node *vn;
+	vn = value_node_find(parent, name);
+	if (!vn)
+		vn = value_node_add(parent, parent, name, NULL);
+	return vn;
+}
+
+struct value_node *value_node_find_by_idx(struct value_node *parent, int idx)
+{
+	struct value_node *vn;
+	llist_for_each_entry(vn, &parent->children, list) {
+		if (idx == vn->idx)
+			return vn;
+	}
+	return NULL;
+}
+
+
+void value_node_del(struct value_node *node)
+{
+	/* remove ourselves from the parent */
+	llist_del(&node->list);
+
+#if 0	/* not actually needed, talloc should do this */
+	struct value_node *ch, *ch2;
+	llist_for_each_entry_safe(ch, ch2, &node->children, list)
+		value_node_del(ch);
+	/* "value" is a talloc child, and "name" is not owned by us */
+#endif
+	/* let talloc do its magic to delete all child nodes */
+	talloc_free(node);
+}
diff --git a/src/value_node.h b/src/value_node.h
new file mode 100644
index 0000000..b8183ba
--- /dev/null
+++ b/src/value_node.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <osmocom/core/linuxlist.h>
+
+/* a single node in the tree of values */
+struct value_node {
+	/* our element in the parent list */
+	struct llist_head list;
+	/* the display name */
+	const char *name;
+	/* additional numeric index (for ifindex matching) */
+	int idx;
+	/* the value (if any) */
+	const char *value;
+	/* the children (if value == NULL) */
+	struct llist_head children;
+};
+
+struct value_node *value_node_add(void *ctx, struct value_node *parent,
+				  const char *name, const char *value);
+struct value_node *value_node_find(struct value_node *parent, const char *name);
+struct value_node *value_node_find_by_idx(struct value_node *parent, int idx);
+struct value_node *value_node_find_or_add(struct value_node *parent, const char *name);
+void value_node_del(struct value_node *node);