Skip to content
Open
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
3 changes: 3 additions & 0 deletions board/common/rootfs/etc/profile.d/update-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
if [ -f /run/infix-update ]; then
printf '\n\033[1;33m *** %s ***\033[0m\n\n' "$(cat /run/infix-update)"
fi
58 changes: 58 additions & 0 deletions board/common/rootfs/usr/sbin/infix-check-update
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/bin/sh
# Check for available firmware updates and notify on login if one exists.
# Called by the scheduler when predefined-action infix-schedule:check-update fires.

NOTIFY_FILE=/run/infix-update
TAG=infix-update

# Source os-release for VERSION and IMAGE_ID
if [ ! -f /etc/os-release ]; then
logger -t "$TAG" "ERROR: /etc/os-release not found"
exit 1
fi
. /etc/os-release

# Dev/dirty builds have no comparable semver — always show the latest release
IS_RELEASE=true
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
IS_RELEASE=false
fi

# Read configured update-url from sysrepo, fall back to upstream
UPDATE_URL=$(sysrepocfg -d running -f json \
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use jq here instead of calls to misc shell programs, this to limit forking:
like this (This is pure AI, not tested, just to give you a hint what i mean):

 UPDATE_URL=$(sysrepocfg -d running -f json \                                                                                           
     -x '/ietf-system:system/infix-system:software/check-update/update-url' \                                                                            
     2>/dev/null \                                                                                                                                       
     | jq -r '.. | objects | ."update-url"? // empty')                                                                                                   

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should look to use the 'copy' shell command instead of sysrepocfg here, since there will be problem calling sysrepocfg for everone except root:

admin@infix-00-00-00:$ sysrepocfg -X -f json -d operational
[ERR] open() on "/etc/sysrepo/sr_main_lock" failed (Permission denied).
sysrepocfg error: Failed to connect (System function call failed)
admin@infix-00-00-00:
$

copy also respect nacm if i remember it correctly.

-x '/ietf-system:system/infix-system:software/check-update/update-url' \
2>/dev/null \
| grep -o '"update-url": *"[^"]*"' \
| grep -o '"[^"]*"$' \
| tr -d '"')
UPDATE_URL=${UPDATE_URL:-"https://github.com/kernelkit/infix"}

# Derive API URL from the configured update URL.
# Default (github.com): https://github.com/org/repo → https://api.github.com/repos/org/repo
REPO=$(echo "$UPDATE_URL" | sed 's|https://github.com/||; s|/*$||')
API_URL="https://api.github.com/repos/${REPO}/releases/latest"

LATEST_TAG=$(curl -sSL --max-time 10 "$API_URL" 2>/dev/null \
| sed -n 's/.*"tag_name" *: *"\([^"]*\)".*/\1/p' \
| head -1)
if [ -z "$LATEST_TAG" ]; then
logger -p daemon.info -t "$TAG" "Update check skipped: could not reach ${API_URL}"
exit 0
fi
LATEST=${LATEST_TAG#v}

# Compare: is $1 strictly newer than $2?
newer() {
[ "$1" = "$2" ] && return 1
[ "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -1)" = "$1" ]
}

if [ "$IS_RELEASE" = false ] || newer "$LATEST" "$VERSION"; then
BUNDLE_URL="${UPDATE_URL}/releases/download/${LATEST_TAG}/${IMAGE_ID}-${LATEST}.tar.gz"
MSG="Firmware update available: ${LATEST_TAG}, running ${VERSION} (see ${BUNDLE_URL})"
logger -t "$TAG" "$MSG — ${BUNDLE_URL}"
printf '%s\n' "$MSG" > "$NOTIFY_FILE"
else
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)"
rm -f "$NOTIFY_FILE"
fi
10 changes: 10 additions & 0 deletions doc/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@ Change Log

All notable changes to the project are documented in this file.

[v26.06.0][] - [[UNRELEASED]]
-------------------------

### Changes

- Added ietf-schedule (basic recurennce) implementation (RFC9922)

### Fixes


[v26.05.0][] - 2026-05-29
-------------------------

Expand Down
2 changes: 1 addition & 1 deletion package/confd/confd.mk
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ else
CONFD_CONF_OPTS += --disable-gps
endif
define CONFD_INSTALL_EXTRA
for fn in confd.conf resolvconf.conf; do \
for fn in confd.conf crond.conf resolvconf.conf; do \
cp $(CONFD_PKGDIR)/$$fn $(FINIT_D)/available/; \
ln -sf ../available/$$fn $(FINIT_D)/enabled/$$fn; \
done
Expand Down
2 changes: 2 additions & 0 deletions package/confd/crond.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Cron daemon for infix-schedule
service [S12345] crond -f -- Cron daemon
Copy link
Copy Markdown
Contributor

@mattiaswal mattiaswal Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not start in runlevel S, 2345 should be enough. This since we have seen slow platforms (arm32) struggle with starting to much, when it should focus on confd.

1 change: 1 addition & 0 deletions src/confd/src/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ confd_plugin_la_SOURCES = \
if-wireguard.c \
keystore.c \
system.c \
schedule.c \
ntp.c \
ptp.c \
syslog.c \
Expand Down
14 changes: 14 additions & 0 deletions src/confd/src/core.c
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
if ((rc = system_change(session, config, diff, event, confd)))
goto free_diff;

/* infix-schedule */
if ((rc = schedule_change(session, config, diff, event, confd)))
goto free_diff;

/* infix-containers */
#ifdef CONTAINERS
if ((rc = containers_change(session, config, diff, event, confd)))
Expand Down Expand Up @@ -794,6 +798,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
ERROR("Failed to subscribe to ietf-hardware");
goto err;
}
rc = subscribe_model("infix-schedule", &confd, 0);
if (rc) {
ERROR("Failed to subscribe to infix-schedule");
goto err;
}
rc = subscribe_model("infix-firewall", &confd, 0);
if (rc) {
ERROR("Failed to subscribe to infix-firewall");
Expand Down Expand Up @@ -858,6 +867,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
rc = ntp_candidate_init(&confd);
if (rc)
goto err;

rc = schedule_init(&confd);
if (rc)
goto err;

/* YOUR_INIT GOES HERE */

return SR_ERR_OK;
Expand Down
4 changes: 4 additions & 0 deletions src/confd/src/core.h
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ int system_rpc_init (struct confd *confd);
int hostnamefmt (struct confd *confd, const char *fmt, char *hostnm, size_t hostlen, char *domain, size_t domlen);
int system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);

/* schedule.c */
int schedule_init(struct confd *confd);
int schedule_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);

/* containers.c */
#ifdef CONTAINERS
int containers_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
Expand Down
225 changes: 225 additions & 0 deletions src/confd/src/schedule.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/* SPDX-License-Identifier: BSD-3-Clause */

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

#include <libite/lite.h>
#include <srx/common.h>
#include <srx/lyx.h>
#include <srx/srx_val.h>
#include "core.h"

#define XPATH_BASE "/ietf-system:system/infix-schedule:schedules"
#define CRONTAB_FILE "/var/spool/cron/crontabs/root"

static const char *action_to_cmd(const char *action)
{
if (!action)
return NULL;
if (strstr(action, "reboot"))
return "/usr/sbin/reboot";
if (strstr(action, "check-update"))
return "/usr/sbin/infix-check-update";
return NULL;
}

/*
* Convert ietf-schedule recurrence to a 5-field cron expression.
*
* Frequency mapping:
* secondly → * * * * * (cron minimum is 1 minute)
* minutely/N → *\/N * * * *
* hourly/N → 0 *\/N * * *
* daily/N → 0 0 *\/N * *
* weekly/N → 0 0 * * *\/N
* monthly/N → 0 0 1 *\/N *
*
* Optional by-* leaves refine the expression:
* byminute → replaces the minute field
* byhour → replaces the hour field
* byday → replaces the day-of-week field
*/
static void build_cron_expr(struct lyd_node *recurrence, char *expr, size_t sz)
{
const char *freq, *ivstr;
struct lyd_node *node;
char min[64], hr[64], dom[64], mon[64], dow[64];
int iv, first;

snprintf(min, sizeof(min), "*");
snprintf(hr, sizeof(hr), "*");
snprintf(dom, sizeof(dom), "*");
snprintf(mon, sizeof(mon), "*");
snprintf(dow, sizeof(dow), "*");

freq = lydx_get_cattr(recurrence, "frequency");
ivstr = lydx_get_cattr(recurrence, "interval");
if (!freq || !ivstr)
goto done;

iv = atoi(ivstr);
if (iv <= 0)
iv = 1;

if (strstr(freq, "secondly")) {
Copy link
Copy Markdown
Contributor

@mattiaswal mattiaswal Jun 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it not possible to set in cron, it should not be possible to set in YANG, block it in yang model, the problematic here is that it is an identity, do not remember how we did with unsupported identities. Lets discuss this.

We have as a policy that everything should be validated in the yang model (using deviate or must expression, mandatory etc), to keep the c-code as little as possible.

/* Sub-minute intervals cannot be expressed in standard cron.
* Map to every minute as the finest available granularity. */
snprintf(min, sizeof(min), "*");
} else if (strstr(freq, "minutely")) {
if (iv == 1)
snprintf(min, sizeof(min), "*");
else
snprintf(min, sizeof(min), "*/%d", iv);
} else if (strstr(freq, "hourly")) {
snprintf(min, sizeof(min), "0");
if (iv == 1)
snprintf(hr, sizeof(hr), "*");
else
snprintf(hr, sizeof(hr), "*/%d", iv);
} else if (strstr(freq, "daily")) {
snprintf(min, sizeof(min), "0");
snprintf(hr, sizeof(hr), "0");
if (iv > 1)
snprintf(dom, sizeof(dom), "*/%d", iv);
} else if (strstr(freq, "weekly")) {
snprintf(min, sizeof(min), "0");
snprintf(hr, sizeof(hr), "0");
if (iv == 1)
snprintf(dow, sizeof(dow), "*");
else
snprintf(dow, sizeof(dow), "*/%d", iv);
} else if (strstr(freq, "monthly")) {
snprintf(min, sizeof(min), "0");
snprintf(hr, sizeof(hr), "0");
snprintf(dom, sizeof(dom), "1");
if (iv > 1)
snprintf(mon, sizeof(mon), "*/%d", iv);
}

/* byminute: override minute field with explicit list */
first = 1;
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byminute") {
const char *val = lyd_get_value(node);
if (!val) continue;
if (first) { snprintf(min, sizeof(min), "%s", val); first = 0; }
else strncat(min, ",", sizeof(min) - strlen(min) - 1),
strncat(min, val, sizeof(min) - strlen(min) - 1);
}

/* byhour: override hour field with explicit list */
first = 1;
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byhour") {
const char *val = lyd_get_value(node);
if (!val) continue;
if (first) { snprintf(hr, sizeof(hr), "%s", val); first = 0; }
else strncat(hr, ",", sizeof(hr) - strlen(hr) - 1),
strncat(hr, val, sizeof(hr) - strlen(hr) - 1);
}

/* byday: override day-of-week field */
first = 1;
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byday") {
const char *val = lydx_get_cattr(node, "weekday");
const char *num = NULL;
if (!val) continue;
/* map YANG weekday names to cron numbers (0=sunday) */
if (!strcmp(val, "sunday")) num = "0";
else if (!strcmp(val, "monday")) num = "1";
else if (!strcmp(val, "tuesday")) num = "2";
else if (!strcmp(val, "wednesday")) num = "3";
else if (!strcmp(val, "thursday")) num = "4";
else if (!strcmp(val, "friday")) num = "5";
else if (!strcmp(val, "saturday")) num = "6";
if (!num) continue;
if (first) { snprintf(dow, sizeof(dow), "%s", num); first = 0; }
else strncat(dow, ",", sizeof(dow) - strlen(dow) - 1),
strncat(dow, num, sizeof(dow) - strlen(dow) - 1);
}

done:
snprintf(expr, sz, "%s %s %s %s %s", min, hr, dom, mon, dow);
}

static void reload_crond(void)
{
char *args[] = { "pkill", "-HUP", "crond", NULL };

runbg(args, 0);
}

static void apply_schedules(struct lyd_node *config)
{
struct lyd_node *schedules, *sched;
FILE *fp;
int count = 0;

makepath("/var/spool/cron/crontabs");
fp = fopen(CRONTAB_FILE, "w");
if (!fp) {
ERROR("schedule: failed to open %s", CRONTAB_FILE);
return;
}
fprintf(fp, "# Managed by infix-schedule\n");

schedules = config ? lydx_get_xpathf(config, XPATH_BASE) : NULL;
if (!schedules)
goto out;

LYX_LIST_FOR_EACH(lyd_child(schedules), sched, "schedule") {
struct lyd_node *recurrence;
const char *name, *action, *cmd;
char expr[128];

if (!lydx_is_enabled(sched, "enabled"))
continue;

name = lydx_get_cattr(sched, "name");

recurrence = lydx_get_child(sched, "recurrence");
if (!recurrence)
continue;

build_cron_expr(recurrence, expr, sizeof(expr));

action = lydx_get_cattr(sched, "predefined-action");
cmd = action_to_cmd(action);
if (!cmd)
continue;

fprintf(fp, "# %s\n%s %s\n", name ?: "unnamed", expr, cmd);
NOTE("schedule: %s → cron '%s %s'", name ?: "unnamed", expr, cmd);
count++;
}

out:
fclose(fp);
reload_crond();
NOTE("schedule: %d active job(s) written to crontab", count);
}

int schedule_change(sr_session_ctx_t *session, struct lyd_node *config,
struct lyd_node *diff, sr_event_t event, struct confd *confd)
{
if (event != SR_EV_DONE)
return SR_ERR_OK;

apply_schedules(config);
return SR_ERR_OK;
}

int schedule_init(struct confd *confd)
{
sr_data_t *data = NULL;

/*
* Sync the crontab with current running config at startup so
* scheduled jobs survive across reboots.
*/
sr_get_data(confd->session, "//.", 0, 0, 0, &data);
apply_schedules(data ? data->tree : NULL);
if (data)
sr_release_data(data);

return SR_ERR_OK;
}
2 changes: 2 additions & 0 deletions src/confd/yang/confd.inc
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,6 @@ MODULES=(
"ieee1588-ptp-tt@2023-08-14.yang -e timestamp-correction"
"ieee802-dot1as-gptp@2025-12-10.yang"
"infix-ptp@2026-04-07.yang"
"ietf-schedule@2026-03-10.yang -e basic-recurrence"
"infix-schedule@2026-05-27.yang"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this file you also need to update the reference to all updated yang models, in this case infix-system-software@.....yang

Loading