statd: break out operational status into new daemon

This patch does two main things:
1) Breaks "statd" free from "confd".
2) Introduces netlink monitoring of interfaces for sysrepo data.

Prior to this patch, sysrepo operation status was handled by code
named "statd" inside "confd". (Where confd is a sysrepo-plugin and
not a real daemon, which its name implies)

In this patch, we break out the interface part of the operational
sysrepo status, previously "statd", into a new daemon, also called
"statd".

The main reason for doing this is to avoid sysrepo threading, which
allows the new "statd" to have control over sysrepo events in the
same event pool as netlink events from the kernel. This in turn allows
statd to subscribe and unsubscribe dynamically to interfaces that
comes and goes during runtime (such as vlans).

It also makes sense that "statd" is in fact a finit daemon.

Signed-off-by: Richard Alpe <richard@bit42.se>
This commit is contained in:
Richard Alpe
2023-07-04 17:43:16 +02:00
committed by Joachim Wiberg
parent e26d4663d0
commit b3213c6228
15 changed files with 568 additions and 15 deletions
+1
View File
@@ -105,6 +105,7 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_SULOGIN=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
+1
View File
@@ -110,6 +110,7 @@ BR2_PACKAGE_HOST_UBOOT_TOOLS=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y
BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SIGNATURE_SUPPORT=y
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_SULOGIN=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
+1
View File
@@ -59,6 +59,7 @@ BR2_PACKAGE_SYSKLOGD_LOGGER=y
BR2_TARGET_ROOTFS_SQUASHFS=y
# BR2_TARGET_ROOTFS_TAR is not set
BR2_PACKAGE_CONFD=y
BR2_PACKAGE_STATD=y
BR2_PACKAGE_FACTORY=y
BR2_PACKAGE_FINIT_SULOGIN=y
BR2_PACKAGE_FINIT_PLUGIN_HOTPLUG=y
+1
View File
@@ -1,4 +1,5 @@
source "$BR2_EXTERNAL_INFIX_PATH/package/confd/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/statd/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/conmon/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/factory/Config.in"
source "$BR2_EXTERNAL_INFIX_PATH/package/faux/Config.in"
+7
View File
@@ -0,0 +1,7 @@
config BR2_PACKAGE_STATD
bool "statd"
help
Operational Status Daemon. Responsible for handling sysrepo
operational run-time info. Such as interface state and address.
All data supplied should be fetched from the running system,
which might differ from data configured through confd.
+1
View File
@@ -0,0 +1 @@
service name:statd log [S12345789] <pid/sysrepo> statd -f -p /run/statd.pid -n -- Status daemon
+28
View File
@@ -0,0 +1,28 @@
################################################################################
#
# statd
#
################################################################################
STATD_VERSION = 1.0
STATD_LICENSE = MIT
STATD_SITE_METHOD = local
STATD_SITE = $(BR2_EXTERNAL_INFIX_PATH)/src/statd
STATD_DEPENDENCIES = sysrepo libev jansson
define STATD_BUILD_CMDS
$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) -C $(@D) \
LDFLAGS="$(TARGET_LDFLAGS)"
endef
define STATD_INSTALL_TARGET_CMDS
$(TARGET_MAKE_ENV) $(TARGET_CONFIGURE_OPTS) $(MAKE) -C $(@D) \
DESTDIR="$(TARGET_DIR)" install
endef
define STATD_INSTALL_EXTRA
cp $(STATD_PKGDIR)/statd.conf $(FINIT_D)/available/
ln -sf ../available/statd.conf $(FINIT_D)/enabled/statd.conf
endef
STATD_TARGET_FINALIZE_HOOKS += STATD_INSTALL_EXTRA
$(eval $(generic-package))
-1
View File
@@ -10,7 +10,6 @@ AC_CONFIG_FILES([
Makefile
src/Makefile
src/confd/Makefile
src/statd/Makefile
yang/Makefile
])
+1 -1
View File
@@ -1,3 +1,3 @@
AUTOMAKE_OPTIONS = subdir-objects
SUBDIRS = confd statd
SUBDIRS = confd
+5
View File
@@ -13,6 +13,11 @@
#include <sys/wait.h>
#include <libite/lite.h>
/* TODO remove once confd / statd lib situation is resolved */
#ifndef vasprintf
int vasprintf(char **strp, const char *fmt, va_list ap);
#endif
/*
* Run cmd in background after delay microseconds.
*/
+4
View File
@@ -3,6 +3,10 @@
#ifndef CONFD_HELPERS_H_
#define CONFD_HELPERS_H_
#include <stdarg.h>
int vasprintf(char **strp, const char *fmt, va_list ap);
int runbg(char *const args[], int delay);
int run_status(int pid);
-13
View File
@@ -1,13 +0,0 @@
CFLAGS += -Wall -Wextra -Werror -Wno-unused-parameter
AM_CPPFLAGS = -D_DEFAULT_SOURCE -D_XOPEN_SOURCE -D_GNU_SOURCE -DYANG_PATH_=\"$(YANGDIR)\"
plugindir = $(srpdplugindir)
plugin_LTLIBRARIES = statd-plugin.la
statd_plugin_la_CFLAGS = $(augeas_CFLAGS) $(jansson_CFLAGS) $(libite_CFLAGS) $(sysrepo_CFLAGS) $(CFLAGS)
statd_plugin_la_LIBADD = $(augeas_LIBS) $(jansson_LIBS) $(libite_LIBS) $(sysrepo_LIBS)
statd_plugin_la_LDFLAGS = -module -avoid-version -shared
statd_plugin_la_SOURCES = core.c ../lib/common.h \
../lib/helpers.h ../lib/helpers.c \
../lib/lyx.h ../lib/lyx.c \
../lib/srx_module.h ../lib/srx_module.c
+27
View File
@@ -0,0 +1,27 @@
# TODO: Libs are included fron confd. They should be broken out of confd and
# shared between statd and confd in some nice way.
CONFD_LIB = $(BASE_DIR)/../src/confd/lib/
CONFD_SRC_LIB = $(BASE_DIR)/../src/confd/src/lib
CFLAGS += -Wall -Wextra -Werror
LDLIBS += -lsysrepo -lev -lyang -ljansson -lite
CPPFLAGS := -I$(CONFD_SRC_LIB)
TARGET = statd
SRC = statd.c
LIB_SRC = $(CONFD_SRC_LIB)/helpers.c $(CONFD_SRC_LIB)/lyx.c $(CONFD_LIB)/vasprintf.c
LIB_HDR = $(CONFD_SRC_LIB)/helpers.h $(CONFD_SRC_LIB)/lyx.h
all: $(TARGET)
$(TARGET): $(SRC) $(LIB_SRC) $(LIB_HDR)
$(CC) $(CPPFLAGS) $(LDLIBS) $(CFLAGS) -o $(TARGET) $(SRC) $(LIB_SRC)
clean:
-rm statd
distclean: clean
-rm *~
install:
install -D statd $(DESTDIR)/sbin/
+491
View File
@@ -0,0 +1,491 @@
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sysrepo.h>
#include <ev.h>
#include <string.h>
#include <errno.h>
#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <jansson.h>
#include <ctype.h>
#include <linux/if.h>
#include <sys/queue.h>
#include "common.h"
#include "helpers.h"
#include "lyx.h"
#define XPATH_MAX PATH_MAX
#define XPATH_IFACE_BASE "/ietf-interfaces:interfaces"
#define SOCK_RMEM_SIZE 1000000 /* Arbitrary chosen, default = 212992 */
#define NL_BUF_SIZE 4096 /* Arbitrary chosen */
TAILQ_HEAD(sub_head, sub);
/* This should, with some modifications, be able to hold other subscription
* types, not only interfaces.
*/
struct sub {
char ifname[IFNAMSIZ];
struct ev_io watcher;
sr_subscription_ctx_t *sr_sub;
TAILQ_ENTRY(sub) entries;
};
struct netlink {
int sd;
struct ev_io watcher;
};
struct statd {
struct netlink nl;
struct ev_loop *ev_loop;
struct sub_head subs;
sr_session_ctx_t *sr_ses;
};
static void set_sock_rcvbuf(int sd, int size)
{
if (setsockopt(sd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)) < 0) {
perror("setsockopt");
return;
}
DEBUG("Socket receive buffer size increased to: %d bytes\n", size);
}
static void str_to_lowercase(char *str)
{
for (int i = 0; str[i]; i++)
str[i] = tolower((unsigned char)str[i]);
}
static int nl_sock_init(void)
{
struct sockaddr_nl addr;
int sock;
sock = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (sock < 0) {
ERROR("Error, creating netlink socket: %s\n", strerror(errno));
return -1;
}
memset(&addr, 0, sizeof(addr));
addr.nl_family = AF_NETLINK;
addr.nl_groups = RTMGRP_LINK;
if (bind(sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
ERROR("Error, binding netlink socket: %s\n", strerror(errno));
close(sock);
return -1;
}
return sock;
}
static struct sub *sub_find_iface(struct sub_head *subs, const char *ifname)
{
struct sub *sub;
TAILQ_FOREACH(sub, subs, entries) {
if (strcmp(sub->ifname, ifname) == 0)
return sub;
}
return NULL;
}
static void sub_delete(struct ev_loop *loop, struct sub_head *subs, struct sub *sub)
{
TAILQ_REMOVE(subs, sub, entries);
ev_io_stop(loop, &sub->watcher);
sr_unsubscribe(sub->sr_sub);
free(sub);
}
static json_t *json_get_ip_link(char *ifname)
{
char cmd[512] = {}; /* Size is arbitrary */
json_error_t j_err;
json_t *j_root;
FILE *proc;
if (ifname)
snprintf(cmd, sizeof(cmd), "ip -d -j link show dev %s 2>/dev/null", ifname);
else
snprintf(cmd, sizeof(cmd), "ip -d -j link show 2>/dev/null");
proc = popenf("re", cmd);
if (!proc) {
ERROR("Error, running ip link command");
return NULL;
}
j_root = json_loadf(proc, 0, &j_err);
pclose(proc);
if (!j_root) {
ERROR("Error, parsing ip link JSON");
return NULL;
}
if (!json_is_array(j_root)) {
ERROR("Expected a JSON array from ip link");
json_decref(j_root);
return NULL;
}
return j_root;
}
static int ly_add_ip_link_data(const struct ly_ctx *ctx, struct lyd_node **parent,
char *xpath, json_t *iface)
{
json_t *j_val;
char *val;
int err;
j_val = json_object_get(iface, "ifindex");
if (!json_is_integer(j_val)) {
ERROR("Expected a JSON integer for 'ifindex'");
return SR_ERR_SYS;
}
err = lydx_new_path(ctx, parent, xpath, "if-index", "%lld",
json_integer_value(j_val));
if (err) {
ERROR("Error, adding 'if-index' to data tree, libyang error %d", err);
return SR_ERR_LY;
}
j_val = json_object_get(iface, "operstate");
if (!json_is_string(j_val)) {
ERROR("Expected a JSON string for 'operstate'");
return SR_ERR_SYS;
}
val = strdup(json_string_value(j_val));
if (!val)
return SR_ERR_SYS;
str_to_lowercase(val);
err = lydx_new_path(ctx, parent, xpath, "oper-status", val);
free(val);
if (err) {
ERROR("Error, adding 'oper-status' to data tree, libyang error %d", err);
return SR_ERR_LY;
}
return SR_ERR_OK;
}
static int ly_add_ip_link(const struct ly_ctx *ctx, struct lyd_node **parent, char *ifname)
{
char xpath[XPATH_MAX] = {};
json_t *j_root;
json_t *j_iface;
int err;
j_root = json_get_ip_link(ifname);
if (!j_root) {
ERROR("Error, parsing ip-link JSON");
return SR_ERR_SYS;
}
if (json_array_size(j_root) != 1) {
ERROR("Error, expected JSON array of single iface");
json_decref(j_root);
return SR_ERR_SYS;
}
j_iface = json_array_get(j_root, 0);
snprintf(xpath, sizeof(xpath), "%s/interface[name='%s']",
XPATH_IFACE_BASE, ifname);
err = ly_add_ip_link_data(ctx, parent, xpath, j_iface);
if (err) {
ERROR("Error, adding ip-link info for %s", ifname);
json_decref(j_root);
return err;
}
json_decref(j_root);
return SR_ERR_OK;
}
static int sr_ifaces_cb(sr_session_ctx_t *session, uint32_t, const char *,
const char *, const char *, uint32_t,
struct lyd_node **parent, void *priv)
{
struct sub *sub = priv;
const struct ly_ctx *ctx;
sr_conn_ctx_t *con;
int err;
DEBUG("Incoming query for xpath: %s\n", path);
con = sr_session_get_connection(session);
if (!con) {
ERROR("Error, getting connection\n");
return SR_ERR_INTERNAL;
}
ctx = sr_acquire_context(con);
if (!ctx) {
ERROR("Error, acquiring context\n");
return SR_ERR_INTERNAL;
}
err = ly_add_ip_link(ctx, parent, sub->ifname);
if (err)
ERROR("Error, adding ip link info\n");
sr_release_context(con);
return err;
}
static void sig_event_cb(struct ev_loop *loop, struct ev_signal *, int)
{
ev_break(loop, EVBREAK_ALL);
}
static void sr_event_cb(struct ev_loop *, struct ev_io *w, int)
{
struct sub *sub = (struct sub *)w->data;
sr_subscription_process_events(sub->sr_sub, NULL, NULL);
}
static int sub_to_iface(struct statd *statd, const char *ifname)
{
char path[XPATH_MAX] = {};
struct sub *sub;
int sr_ev_pipe;
int err;
sub = sub_find_iface(&statd->subs, ifname);
if (sub) {
DEBUG("Interface %s already subscribed\n", ifname);
return SR_ERR_OK;
}
sub = malloc(sizeof(struct sub));
if (!sub)
return SR_ERR_INTERNAL;
memset(sub, 0, sizeof(struct sub));
snprintf(sub->ifname, sizeof(sub->ifname), "%s", ifname);
snprintf(path, sizeof(path), "%s/interface[name='%s']",
XPATH_IFACE_BASE, ifname);
DEBUG("Subscribe to events for \"%s\"\n", path);
err = sr_oper_get_subscribe(statd->sr_ses, "ietf-interfaces",
path, sr_ifaces_cb, sub,
SR_SUBSCR_DEFAULT | SR_SUBSCR_NO_THREAD | SR_SUBSCR_DONE_ONLY,
&sub->sr_sub);
if (err) {
ERROR("Error, subscribing to path \"%s\": %s\n", path, sr_strerror(err));
free(sub);
return SR_ERR_INTERNAL;
}
err = sr_get_event_pipe(sub->sr_sub, &sr_ev_pipe);
if (err) {
ERROR("Error, getting sysrepo event pipe: %s\n", sr_strerror(err));
sr_unsubscribe(sub->sr_sub);
free(sub);
return SR_ERR_INTERNAL;
}
TAILQ_INSERT_TAIL(&statd->subs, sub, entries);
ev_io_init(&sub->watcher, sr_event_cb, sr_ev_pipe, EV_READ);
sub->watcher.data = sub;
ev_io_start(statd->ev_loop, &sub->watcher);
return SR_ERR_OK;
}
static void unsub_to_ifaces(struct statd *statd)
{
struct sub *sub;
while (!TAILQ_EMPTY(&statd->subs)) {
sub = TAILQ_FIRST(&statd->subs);
DEBUG("Unsubscribe from \"%s\" (all)\n", sub->ifname);
sub_delete(statd->ev_loop, &statd->subs, sub);
}
}
static int unsub_to_iface(struct statd *statd, char *ifname)
{
struct sub *sub;
sub = sub_find_iface(&statd->subs, ifname);
if (!sub) {
ERROR("Error, can't find interface to delete (%s)", ifname);
return SR_ERR_INTERNAL;
}
DEBUG("Unsubscribe from \"%s\"\n", sub->ifname);
sub_delete(statd->ev_loop, &statd->subs, sub);
return SR_ERR_OK;
}
static int nl_process_msg(struct nlmsghdr *nlh, struct statd *statd)
{
struct ifinfomsg *iface;
struct rtattr *attr;
int attr_len;
iface = NLMSG_DATA(nlh);
attr = IFLA_RTA(iface);
attr_len = IFLA_PAYLOAD(nlh);
for (; RTA_OK(attr, attr_len); attr = RTA_NEXT(attr, attr_len)) {
if (attr->rta_type == IFLA_IFNAME) {
char *ifname = (char *)RTA_DATA(attr);
if (nlh->nlmsg_type == RTM_NEWLINK)
return sub_to_iface(statd, ifname);
else if (nlh->nlmsg_type == RTM_DELLINK)
return unsub_to_iface(statd, ifname);
else
return SR_ERR_INTERNAL;
}
}
/* Ignore nl messages with no interface name */
return SR_ERR_OK;
}
static void nl_event_cb(struct ev_loop *, struct ev_io *w, int)
{
struct statd *statd = (struct statd *) w->data;
char buf[NL_BUF_SIZE];
struct nlmsghdr *nlh;
int err;
int len;
len = recv(statd->nl.sd, buf, sizeof(buf), 0);
if (len < 0) {
ERROR("Error, netlink recv failed: %s\n", strerror(errno));
close(statd->nl.sd);
/* NOTE: This is likely caused by a full kernel buffer, which
* means we can't trust our list. So we exit hard and let finit
* respawn us to handle this.
*/
exit(EXIT_FAILURE);
}
for (nlh = (struct nlmsghdr *)buf; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) {
err = nl_process_msg(nlh, statd);
if (err)
ERROR("Error, processing netlink message: %s\n", sr_strerror(err));
}
}
static int sub_to_ifaces(struct statd *statd)
{
json_t *j_iface;
json_t *j_root;
size_t i;
j_root = json_get_ip_link(NULL);
if (!j_root) {
ERROR("Error, parsing ip-link JSON");
return SR_ERR_SYS;
}
json_array_foreach(j_root, i, j_iface) {
json_t *j_ifname;
int err;
j_ifname = json_object_get(j_iface, "ifname");
if (!json_is_string(j_ifname)) {
ERROR("Got unexpected JSON type for 'ifname'");
continue;
}
err = sub_to_iface(statd, json_string_value(j_ifname));
if (err) {
ERROR("Unable to subscribe to %s", json_string_value(j_ifname));
continue;
}
}
json_decref(j_root);
return SR_ERR_OK;
}
int main(void)
{
struct ev_signal sig_watcher;
struct statd statd = {};
sr_conn_ctx_t *sr_conn;
int err;
TAILQ_INIT(&statd.subs);
statd.ev_loop = EV_DEFAULT;
statd.nl.sd = nl_sock_init();
if (statd.nl.sd < 0) {
ERROR("Error, opening netlink socket\n");
return EXIT_FAILURE;
}
INFO("Status daemon starting\n");
set_sock_rcvbuf(statd.nl.sd, SOCK_RMEM_SIZE);
err = sr_connect(SR_CONN_DEFAULT, &sr_conn);
if (err) {
ERROR("Error, connecting to sysrepo: %s\n", sr_strerror(err));
return EXIT_FAILURE;
}
DEBUG("Connected to sysrepo\n");
err = sr_session_start(sr_conn, SR_DS_OPERATIONAL, &statd.sr_ses);
if (err) {
ERROR("Error, start sysrepo session: %s\n", sr_strerror(err));
sr_disconnect(sr_conn);
return EXIT_FAILURE;
}
DEBUG("Session started (%p)\n", statd.sr_ses);
DEBUG("Attempting to register existing interfaces\n");
err = sub_to_ifaces(&statd);
if (err) {
ERROR("Error, registering existing interfaces\n");
sr_disconnect(sr_conn);
return EXIT_FAILURE;
}
ev_signal_init(&sig_watcher, sig_event_cb, SIGINT);
sig_watcher.data = &statd;
ev_signal_start(statd.ev_loop, &sig_watcher);
ev_io_init(&statd.nl.watcher, nl_event_cb, statd.nl.sd, EV_READ);
statd.nl.watcher.data = &statd;
ev_io_start(statd.ev_loop, &statd.nl.watcher);
INFO("Status daemon entering main event loop\n");
ev_run(statd.ev_loop, 0);
/* We should never get here during normal operation */
INFO("Status daemon shutting down\n");
unsub_to_ifaces(&statd);
sr_session_stop(statd.sr_ses);
sr_disconnect(sr_conn);
return EXIT_SUCCESS;
}