Skip to content

Commit da0d85f

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

22 files changed

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

0 commit comments

Comments
 (0)