Skip to content

Commit 96ab233

Browse files
committed
feat: infix-schedule based on ietf-schedule(basic) implementation
Signed-off-by: Ejub Sabic <ejub1946@outlook.com>
1 parent 46fb3a0 commit 96ab233

25 files changed

Lines changed: 1907 additions & 2 deletions
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
if [ -s /run/infix-update ]; then
2+
printf '\n\033[1;33m *** %s ***\033[0m\n\n' "$(cat /run/infix-update)"
3+
fi
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
f /run/infix-update 0666 admin admin
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#!/bin/sh
2+
# Check for available firmware updates and notify on login if one exists.
3+
# Called by the scheduler when predefined-action infix-schedule:check-update fires.
4+
5+
NOTIFY_FILE=/run/infix-update
6+
TAG=infix-update
7+
8+
# Source os-release for VERSION and IMAGE_ID
9+
if [ ! -f /etc/os-release ]; then
10+
logger -t "$TAG" "ERROR: /etc/os-release not found"
11+
exit 1
12+
fi
13+
. /etc/os-release
14+
15+
# Dev/dirty builds have no comparable semver — always show the latest release
16+
IS_RELEASE=true
17+
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
18+
IS_RELEASE=false
19+
fi
20+
21+
# Read configured update-url from running config, fall back to upstream
22+
UPDATE_URL=$(copy running-config \
23+
-x '/ietf-system:system/infix-system:software/check-update/update-url' \
24+
2>/dev/null \
25+
| jq -r '.. | objects | ."update-url"? // empty')
26+
UPDATE_URL=${UPDATE_URL:-"https://github.com/kernelkit/infix"}
27+
28+
# Derive API URL from the configured update URL.
29+
# Default (github.com): https://github.com/org/repo → https://api.github.com/repos/org/repo
30+
REPO=$(echo "$UPDATE_URL" | sed 's|https://github.com/||; s|/*$||')
31+
API_URL="https://api.github.com/repos/${REPO}/releases/latest"
32+
33+
LATEST_TAG=$(curl -sSL --max-time 10 "$API_URL" 2>/dev/null \
34+
| sed -n 's/.*"tag_name" *: *"\([^"]*\)".*/\1/p' \
35+
| head -1)
36+
if [ -z "$LATEST_TAG" ]; then
37+
logger -p daemon.info -t "$TAG" "Update check skipped: could not reach ${API_URL}"
38+
exit 0
39+
fi
40+
LATEST=${LATEST_TAG#v}
41+
42+
# Compare: is $1 strictly newer than $2?
43+
newer() {
44+
[ "$1" = "$2" ] && return 1
45+
[ "$(printf '%s\n%s' "$1" "$2" | sort -V | tail -1)" = "$1" ]
46+
}
47+
48+
if [ "$IS_RELEASE" = false ] || newer "$LATEST" "$VERSION"; then
49+
BUNDLE_URL="${UPDATE_URL}/releases/download/${LATEST_TAG}/${IMAGE_ID}-${LATEST}.tar.gz"
50+
MSG="Firmware update available: ${LATEST_TAG}, running ${VERSION} (see ${BUNDLE_URL})"
51+
logger -t "$TAG" "$MSG${BUNDLE_URL}"
52+
printf '%s\n' "$MSG" > "$NOTIFY_FILE"
53+
else
54+
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)"
55+
printf '' > "$NOTIFY_FILE"
56+
fi

doc/ChangeLog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@ Change Log
33

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

6+
[v26.06.0][] - [[UNRELEASED]]
7+
-------------------------
8+
9+
### Changes
10+
11+
- Added ietf-schedule (basic recurennce) implementation (RFC9922)
12+
13+
### Fixes
14+
15+
616
[v26.05.0][] - 2026-05-29
717
-------------------------
818

package/confd/confd.mk

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ else
3636
CONFD_CONF_OPTS += --disable-gps
3737
endif
3838
define CONFD_INSTALL_EXTRA
39-
for fn in confd.conf resolvconf.conf; do \
39+
for fn in confd.conf crond.conf resolvconf.conf; do \
4040
cp $(CONFD_PKGDIR)/$$fn $(FINIT_D)/available/; \
4141
ln -sf ../available/$$fn $(FINIT_D)/enabled/$$fn; \
4242
done

package/confd/crond.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Cron daemon for infix-schedule
2+
service [2345] crond -f -- Cron daemon

src/confd/src/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ confd_plugin_la_SOURCES = \
4848
if-wireguard.c \
4949
keystore.c \
5050
system.c \
51+
schedule.c \
5152
ntp.c \
5253
ptp.c \
5354
syslog.c \

src/confd/src/core.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -621,6 +621,10 @@ static int change_cb(sr_session_ctx_t *session, uint32_t sub_id, const char *mod
621621
if ((rc = system_change(session, config, diff, event, confd)))
622622
goto free_diff;
623623

624+
/* infix-schedule */
625+
if ((rc = schedule_change(session, config, diff, event, confd)))
626+
goto free_diff;
627+
624628
/* infix-containers */
625629
#ifdef CONTAINERS
626630
if ((rc = containers_change(session, config, diff, event, confd)))
@@ -794,6 +798,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
794798
ERROR("Failed to subscribe to ietf-hardware");
795799
goto err;
796800
}
801+
rc = subscribe_model("infix-schedule", &confd, 0);
802+
if (rc) {
803+
ERROR("Failed to subscribe to infix-schedule");
804+
goto err;
805+
}
797806
rc = subscribe_model("infix-firewall", &confd, 0);
798807
if (rc) {
799808
ERROR("Failed to subscribe to infix-firewall");
@@ -858,6 +867,11 @@ int sr_plugin_init_cb(sr_session_ctx_t *session, void **priv)
858867
rc = ntp_candidate_init(&confd);
859868
if (rc)
860869
goto err;
870+
871+
rc = schedule_init(&confd);
872+
if (rc)
873+
goto err;
874+
861875
/* YOUR_INIT GOES HERE */
862876

863877
return SR_ERR_OK;

src/confd/src/core.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,10 @@ int system_rpc_init (struct confd *confd);
215215
int hostnamefmt (struct confd *confd, const char *fmt, char *hostnm, size_t hostlen, char *domain, size_t domlen);
216216
int system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
217217

218+
/* schedule.c */
219+
int schedule_init(struct confd *confd);
220+
int schedule_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);
221+
218222
/* containers.c */
219223
#ifdef CONTAINERS
220224
int containers_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd_node *diff, sr_event_t event, struct confd *confd);

src/confd/src/schedule.c

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/* SPDX-License-Identifier: BSD-3-Clause */
2+
3+
#include <signal.h>
4+
#include <stdio.h>
5+
#include <stdlib.h>
6+
7+
#include <libite/lite.h>
8+
#include <srx/common.h>
9+
#include <srx/lyx.h>
10+
#include <srx/srx_val.h>
11+
#include "core.h"
12+
13+
#define XPATH_BASE "/ietf-system:system/infix-schedule:schedules"
14+
#define CRONTAB_FILE "/var/spool/cron/crontabs/admin"
15+
16+
static const char *action_to_cmd(const char *action)
17+
{
18+
if (!action)
19+
return NULL;
20+
if (strstr(action, "reboot"))
21+
return "/usr/sbin/reboot";
22+
if (strstr(action, "check-update"))
23+
return "/usr/sbin/infix-check-update";
24+
return NULL;
25+
}
26+
27+
/*
28+
* Convert ietf-schedule recurrence to a 5-field cron expression.
29+
*
30+
* Frequency mapping:
31+
* minutely/N → *\/N * * * *
32+
* hourly/N → 0 *\/N * * *
33+
* daily/N → 0 0 *\/N * *
34+
* weekly/N → 0 0 * * *\/N
35+
* monthly/N → 0 0 1 *\/N *
36+
*
37+
* Optional by-* leaves refine the expression:
38+
* byminute → replaces the minute field
39+
* byhour → replaces the hour field
40+
* byday → replaces the day-of-week field
41+
*/
42+
static void build_cron_expr(struct lyd_node *recurrence, char *expr, size_t sz)
43+
{
44+
const char *freq, *ivstr;
45+
struct lyd_node *node;
46+
char min[64], hr[64], dom[64], mon[64], dow[64];
47+
int iv, first;
48+
49+
snprintf(min, sizeof(min), "*");
50+
snprintf(hr, sizeof(hr), "*");
51+
snprintf(dom, sizeof(dom), "*");
52+
snprintf(mon, sizeof(mon), "*");
53+
snprintf(dow, sizeof(dow), "*");
54+
55+
freq = lydx_get_cattr(recurrence, "frequency");
56+
ivstr = lydx_get_cattr(recurrence, "interval");
57+
if (!freq || !ivstr)
58+
goto done;
59+
60+
iv = atoi(ivstr);
61+
if (iv <= 0)
62+
iv = 1;
63+
64+
if (strstr(freq, "minutely")) {
65+
if (iv == 1)
66+
snprintf(min, sizeof(min), "*");
67+
else
68+
snprintf(min, sizeof(min), "*/%d", iv);
69+
} else if (strstr(freq, "hourly")) {
70+
snprintf(min, sizeof(min), "0");
71+
if (iv == 1)
72+
snprintf(hr, sizeof(hr), "*");
73+
else
74+
snprintf(hr, sizeof(hr), "*/%d", iv);
75+
} else if (strstr(freq, "daily")) {
76+
snprintf(min, sizeof(min), "0");
77+
snprintf(hr, sizeof(hr), "0");
78+
if (iv > 1)
79+
snprintf(dom, sizeof(dom), "*/%d", iv);
80+
} else if (strstr(freq, "weekly")) {
81+
snprintf(min, sizeof(min), "0");
82+
snprintf(hr, sizeof(hr), "0");
83+
if (iv == 1)
84+
snprintf(dow, sizeof(dow), "*");
85+
else
86+
snprintf(dow, sizeof(dow), "*/%d", iv);
87+
} else if (strstr(freq, "monthly")) {
88+
snprintf(min, sizeof(min), "0");
89+
snprintf(hr, sizeof(hr), "0");
90+
snprintf(dom, sizeof(dom), "1");
91+
if (iv > 1)
92+
snprintf(mon, sizeof(mon), "*/%d", iv);
93+
}
94+
95+
/* byminute: override minute field with explicit list */
96+
first = 1;
97+
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byminute") {
98+
const char *val = lyd_get_value(node);
99+
if (!val) continue;
100+
if (first) { snprintf(min, sizeof(min), "%s", val); first = 0; }
101+
else strncat(min, ",", sizeof(min) - strlen(min) - 1),
102+
strncat(min, val, sizeof(min) - strlen(min) - 1);
103+
}
104+
105+
/* byhour: override hour field with explicit list */
106+
first = 1;
107+
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byhour") {
108+
const char *val = lyd_get_value(node);
109+
if (!val) continue;
110+
if (first) { snprintf(hr, sizeof(hr), "%s", val); first = 0; }
111+
else strncat(hr, ",", sizeof(hr) - strlen(hr) - 1),
112+
strncat(hr, val, sizeof(hr) - strlen(hr) - 1);
113+
}
114+
115+
/* bymonthday: override day-of-month field */
116+
first = 1;
117+
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "bymonthday") {
118+
const char *val = lyd_get_value(node);
119+
if (!val) continue;
120+
if (first) { snprintf(dom, sizeof(dom), "%s", val); first = 0; }
121+
else strncat(dom, ",", sizeof(dom) - strlen(dom) - 1),
122+
strncat(dom, val, sizeof(dom) - strlen(dom) - 1);
123+
}
124+
125+
/* byyearmonth: override month field */
126+
first = 1;
127+
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byyearmonth") {
128+
const char *val = lyd_get_value(node);
129+
if (!val) continue;
130+
if (first) { snprintf(mon, sizeof(mon), "%s", val); first = 0; }
131+
else strncat(mon, ",", sizeof(mon) - strlen(mon) - 1),
132+
strncat(mon, val, sizeof(mon) - strlen(mon) - 1);
133+
}
134+
135+
/* byday: override day-of-week field */
136+
first = 1;
137+
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byday") {
138+
const char *val = lydx_get_cattr(node, "weekday");
139+
const char *num = NULL;
140+
if (!val) continue;
141+
/* map YANG weekday names to cron numbers (0=sunday) */
142+
if (!strcmp(val, "sunday")) num = "0";
143+
else if (!strcmp(val, "monday")) num = "1";
144+
else if (!strcmp(val, "tuesday")) num = "2";
145+
else if (!strcmp(val, "wednesday")) num = "3";
146+
else if (!strcmp(val, "thursday")) num = "4";
147+
else if (!strcmp(val, "friday")) num = "5";
148+
else if (!strcmp(val, "saturday")) num = "6";
149+
if (!num) continue;
150+
if (first) { snprintf(dow, sizeof(dow), "%s", num); first = 0; }
151+
else strncat(dow, ",", sizeof(dow) - strlen(dow) - 1),
152+
strncat(dow, num, sizeof(dow) - strlen(dow) - 1);
153+
}
154+
155+
done:
156+
snprintf(expr, sz, "%s %s %s %s %s", min, hr, dom, mon, dow);
157+
}
158+
159+
static void reload_crond(void)
160+
{
161+
char *args[] = { "pkill", "-HUP", "crond", NULL };
162+
163+
runbg(args, 0);
164+
}
165+
166+
static void apply_schedules(struct lyd_node *config)
167+
{
168+
struct lyd_node *schedules, *sched;
169+
FILE *fp;
170+
int count = 0;
171+
172+
makepath("/var/spool/cron/crontabs");
173+
fp = fopen(CRONTAB_FILE, "w");
174+
if (!fp) {
175+
ERROR("schedule: failed to open %s", CRONTAB_FILE);
176+
return;
177+
}
178+
fprintf(fp, "# Managed by infix-schedule\n");
179+
180+
schedules = config ? lydx_get_xpathf(config, XPATH_BASE) : NULL;
181+
if (!schedules)
182+
goto out;
183+
184+
LYX_LIST_FOR_EACH(lyd_child(schedules), sched, "schedule") {
185+
struct lyd_node *recurrence;
186+
const char *name, *action, *cmd;
187+
char expr[128];
188+
189+
if (!lydx_is_enabled(sched, "enabled"))
190+
continue;
191+
192+
name = lydx_get_cattr(sched, "name");
193+
194+
recurrence = lydx_get_child(sched, "recurrence");
195+
if (!recurrence)
196+
continue;
197+
198+
build_cron_expr(recurrence, expr, sizeof(expr));
199+
200+
action = lydx_get_cattr(sched, "predefined-action");
201+
cmd = action_to_cmd(action);
202+
if (!cmd)
203+
continue;
204+
205+
fprintf(fp, "# %s\n%s %s\n", name ?: "unnamed", expr, cmd);
206+
NOTE("schedule: %s → cron '%s %s'", name ?: "unnamed", expr, cmd);
207+
count++;
208+
}
209+
210+
out:
211+
fclose(fp);
212+
reload_crond();
213+
NOTE("schedule: %d active job(s) written to crontab", count);
214+
}
215+
216+
int schedule_change(sr_session_ctx_t *session, struct lyd_node *config,
217+
struct lyd_node *diff, sr_event_t event, struct confd *confd)
218+
{
219+
if (event != SR_EV_DONE)
220+
return SR_ERR_OK;
221+
222+
apply_schedules(config);
223+
return SR_ERR_OK;
224+
}
225+
226+
int schedule_init(struct confd *confd)
227+
{
228+
sr_data_t *data = NULL;
229+
230+
/*
231+
* Sync the crontab with current running config at startup so
232+
* scheduled jobs survive across reboots.
233+
*/
234+
sr_get_data(confd->session, "//.", 0, 0, 0, &data);
235+
apply_schedules(data ? data->tree : NULL);
236+
if (data)
237+
sr_release_data(data);
238+
239+
return SR_ERR_OK;
240+
}

0 commit comments

Comments
 (0)