Merge pull request #1523 from saba8814/feat/add-scheduler-support

Add support for ietf-schedule
This commit is contained in:
Joachim Wiberg
2026-06-18 16:54:52 +02:00
committed by GitHub
29 changed files with 1767 additions and 32 deletions
@@ -0,0 +1,3 @@
if [ -s /run/os-update ]; then
printf '\n\033[1;33m *** %s ***\033[0m\n\n' "$(cat /run/os-update)"
fi
@@ -0,0 +1 @@
f /run/os-update 0666 admin admin
+55
View File
@@ -0,0 +1,55 @@
#!/bin/sh
# Check for available software updates and notify on login if one exists.
# Called by the scheduler.
NOTIFY_FILE=/run/os-update
TAG=os-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 running config, fall back to upstream
UPDATE_URL=$(copy running-config \
-x '/ietf-system:system/infix-system:software/check-update/update-url' \
2>/dev/null \
| jq -r '.. | objects | ."update-url"? // empty')
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 \
| jq -r '.tag_name // empty')
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
RELEASE_URL="${UPDATE_URL}/releases/${LATEST_TAG}"
MSG="Software update available: ${LATEST_TAG}, running ${VERSION} (see ${RELEASE_URL})"
logger -t "$TAG" "$MSG"
printf '%s\n' "$MSG" > "$NOTIFY_FILE"
else
logger -p daemon.debug -t "$TAG" "No update available (current: $VERSION, latest: $LATEST)"
printf '' > "$NOTIFY_FILE"
fi
+4
View File
@@ -23,6 +23,10 @@ All notable changes to the project are documented in this file.
clients onto the faster 5/6 GHz band
- Add `legacy-rates` option to re-enable 802.11b rates on 2.4 GHz for
old IoT devices (disabled by default)
- Add system scheduling based on ietf-schedule (RFC 9922), using the
iCalendar recurrence grouping pruned to cron-expressible rules. Schedules
are reusable time-specs; features (`scheduled-reboot`,
`software/check-update`) trigger off them via a schedule reference
### Fixes
+1 -1
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
# Cron daemon for infix-schedule
service [2345] crond -f -- Cron daemon
+1
View File
@@ -50,6 +50,7 @@ confd_plugin_la_SOURCES = \
if-wireguard.c \
keystore.c \
system.c \
schedule.c \
ntp.c \
ptp.c \
syslog.c \
+9
View File
@@ -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)))
@@ -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");
+11
View File
@@ -215,6 +215,17 @@ 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 */
/* A feature registers cron consumer to run a command on a schedule. */
struct cron_consumer {
const char *path; /* xpath of the container holding the schedule-ref leaf */
const char *sched_leaf; /* name of the schedule-ref leaf within 'path' */
const char *enabled_leaf; /* boolean leaf in 'path' that gates the job; NULL = active whenever a schedule is referenced */
const char *command; /* what crond runs on each occurrence */
};
int schedule_consumer_register(const struct cron_consumer *consumer);
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);
+282
View File
@@ -0,0 +1,282 @@
/* 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/admin"
/* Features register a consumer to run a command on a schedule. */
static const struct cron_consumer **consumers;
static size_t consumer_count;
int schedule_consumer_register(const struct cron_consumer *consumer)
{
const struct cron_consumer **vec;
if (!consumer || !consumer->path || !consumer->sched_leaf || !consumer->command)
return -1;
vec = realloc(consumers, (consumer_count + 1) * sizeof(*vec));
if (!vec) {
ERROR("schedule: out of memory registering %s", consumer->path);
return -1;
}
consumers = vec;
consumers[consumer_count++] = consumer;
return 0;
}
/*
* Convert ietf-schedule recurrence to a 5-field cron expression.
*
* Frequency mapping:
* minutely/N → *\/N * * * *
* hourly/N → 0 *\/N * * *
* daily/N → 0 0 *\/N * *
* weekly/N → 0 0 * * *\/N
* monthly/N → 0 0 1 *\/N *
* yearly → 0 0 1 1 * (interval > 1 not expressible in cron)
*
* Optional by-* leaves refine the expression:
* byminute → replaces the minute field
* byhour → replaces the hour field
* byday → replaces the day-of-week field
* bymonthday → replaces the day-of-month field (positive values only, 1-31)
* byyearmonth → replaces the month field
*/
static void build_cron_expr(struct lyd_node *recurrence, char *expr, size_t sz)
{
char min[64], hr[64], dom[64], mon[64], dow[64];
const char *freq, *ivstr;
struct lyd_node *node;
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, "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);
} else if (strstr(freq, "yearly")) {
/* Once a year: midnight Jan 1, refined by byyearmonth/bymonthday.
* "every N years" (iv > 1) has no five-field cron equivalent. */
snprintf(min, sizeof(min), "0");
snprintf(hr, sizeof(hr), "0");
snprintf(dom, sizeof(dom), "1");
snprintf(mon, sizeof(mon), "1");
}
/* 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);
}
/* bymonthday: override day-of-month field */
first = 1;
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "bymonthday") {
const char *val = lyd_get_value(node);
if (!val) continue;
if (first) { snprintf(dom, sizeof(dom), "%s", val); first = 0; }
else strncat(dom, ",", sizeof(dom) - strlen(dom) - 1),
strncat(dom, val, sizeof(dom) - strlen(dom) - 1);
}
/* byyearmonth: override month field */
first = 1;
LYX_LIST_FOR_EACH(lyd_child(recurrence), node, "byyearmonth") {
const char *val = lyd_get_value(node);
if (!val) continue;
if (first) { snprintf(mon, sizeof(mon), "%s", val); first = 0; }
else strncat(mon, ",", sizeof(mon) - strlen(mon) - 1),
strncat(mon, val, sizeof(mon) - strlen(mon) - 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);
}
/*
* Resolve a schedule by name to a cron expression. Returns 0 on success.
* A disabled schedule is the master kill-switch: it resolves to "not active"
* (-1) so every feature referencing it stops firing.
*/
static int schedule_to_cron(struct lyd_node *config, const char *name,
char *expr, size_t sz)
{
struct lyd_node *schedules, *sched;
if (!config || !name)
return -1;
schedules = lydx_get_xpathf(config, XPATH_BASE);
if (!schedules)
return -1;
LYX_LIST_FOR_EACH(lyd_child(schedules), sched, "schedule") {
const char *sname = lydx_get_cattr(sched, "name");
if (!sname || strcmp(sname, name))
continue;
if (!lydx_is_enabled(sched, "enabled"))
return -1;
build_cron_expr(lydx_get_child(sched, "recurrence"), expr, sz);
return 0;
}
return -1;
}
/*
* Rebuild the crontab from every registered consumer. Each consumer points
* at a feature container holding a schedule-ref; we resolve that to cron
* fields and emit one line running the consumer's own command.
*/
static void apply_schedules(struct lyd_node *config)
{
int count = 0;
FILE *fp;
size_t i;
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");
if (!config)
goto out;
for (i = 0; i < consumer_count; i++) {
const struct cron_consumer *c = consumers[i];
struct lyd_node *node;
const char *name;
char expr[128];
node = lydx_get_xpathf(config, "%s", c->path);
if (!node)
continue;
if (c->enabled_leaf && !lydx_is_enabled(node, c->enabled_leaf))
continue;
name = lydx_get_cattr(node, c->sched_leaf);
if (!name)
continue;
if (schedule_to_cron(config, name, expr, sizeof(expr))) {
NOTE("schedule: '%s' references inactive schedule '%s', skipping", c->path, name);
continue;
}
fprintf(fp, "# %s -> %s\n%s %s\n", c->path, name, expr, c->command);
NOTE("schedule: %s → cron '%s %s'", name, expr, c->command);
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 && event != SR_EV_ENABLED)
return SR_ERR_OK;
apply_schedules(config);
return SR_ERR_OK;
}
+10
View File
@@ -89,6 +89,14 @@ static int infix_system_sw_set_boot_order(sr_session_ctx_t *session, uint32_t su
return SR_ERR_OK;
}
/* Scheduler consumer for check-update. */
static const struct cron_consumer check_update_consumer = {
.path = "/ietf-system:system/infix-system:software/check-update",
.sched_leaf = "schedule",
.enabled_leaf = "enabled",
.command = "/usr/sbin/check-update",
};
int system_sw_rpc_init(struct confd *confd)
{
int rc = 0;
@@ -98,6 +106,8 @@ int system_sw_rpc_init(struct confd *confd)
REGISTER_RPC(confd->session, "/infix-system:set-boot-order",
infix_system_sw_set_boot_order, NULL, &confd->sub);
schedule_consumer_register(&check_update_consumer);
fail:
return rc;
}
+10
View File
@@ -1678,6 +1678,14 @@ int system_change(sr_session_ctx_t *session, struct lyd_node *config, struct lyd
return SR_ERR_OK;
}
/* Scheduler consumer for scheduled-reboot. */
static const struct cron_consumer reboot_consumer = {
.path = "/ietf-system:system/infix-system:scheduled-reboot",
.sched_leaf = "schedule",
.enabled_leaf = NULL,
.command = "/usr/sbin/reboot",
};
int system_rpc_init(struct confd *confd)
{
int rc;
@@ -1688,6 +1696,8 @@ int system_rpc_init(struct confd *confd)
REGISTER_RPC(confd->session, "/ietf-system:system-shutdown", rpc_exec, "poweroff", &confd->sub);
REGISTER_RPC(confd->session, "/ietf-system:set-current-datetime", rpc_set_datetime, NULL, &confd->sub);
schedule_consumer_register(&reboot_consumer);
return SR_ERR_OK;
fail:
ERROR("init failed: %s", sr_strerror(rc));
+3 -1
View File
@@ -42,8 +42,8 @@ MODULES=(
"infix-firewall-services@2025-04-26.yang"
"infix-firewall-icmp-types@2025-04-26.yang"
"infix-meta@2025-12-10.yang"
"infix-system@2026-03-09.yang"
"infix-services@2026-06-17.yang"
"infix-system@2026-06-17.yang"
"ieee802-ethernet-interface@2025-09-10.yang"
"ieee802-ethernet-phy-type@2025-09-10.yang"
"infix-ethernet-interface@2026-05-21.yang"
@@ -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 icalendar-recurrence"
"infix-schedule@2026-06-17.yang"
)
@@ -0,0 +1,868 @@
module ietf-schedule {
yang-version 1.1;
namespace "urn:ietf:params:xml:ns:yang:ietf-schedule";
prefix schedule;
import ietf-yang-types {
prefix yang;
reference
"RFC 9911: Common YANG Data Types";
}
import ietf-system {
prefix sys;
reference
"RFC 7317: A YANG Data Model for System Management";
}
organization
"IETF NETMOD Working Group";
contact
"WG Web: <https://datatracker.ietf.org/wg/netmod/>
WG List: <mailto:netmod@ietf.org>
Editor: Qiufang Ma
<mailto:maqiufang1@huawei.com>
Author: Qin Wu
<mailto:bill.wu@huawei.com>
Editor: Mohamed Boucadair
<mailto:mohamed.boucadair@orange.com>
Author: Daniel King
<mailto:d.king@lancaster.ac.uk>";
description
"This YANG module defines a set of common types and groupings
that are applicable for scheduling purposes, such as events,
policies, services, or resources based on date and time.
The key words 'MUST', 'MUST NOT', 'REQUIRED', 'SHALL', 'SHALL
NOT', 'SHOULD', 'SHOULD NOT', 'RECOMMENDED', 'NOT RECOMMENDED',
'MAY', and 'OPTIONAL' in this document are to be interpreted as
described in BCP 14 (RFC 2119) (RFC 8174) when, and only when,
they appear in all capitals, as shown here.
Copyright (c) 2026 IETF Trust and the persons identified
as authors of the code. All rights reserved.
Redistribution and use in source and binary forms, with
or without modification, is permitted pursuant to, and
subject to the license terms contained in, the Revised
BSD License set forth in Section 4.c of the IETF Trust's
Legal Provisions Relating to IETF Documents
(https://trustee.ietf.org/license-info).
This version of this YANG module is part of RFC 9922; see
the RFC itself for full legal notices.
All revisions of IETF and IANA-maintained modules can be found
in the 'YANG Parameters' registry group
(https://www.iana.org/assignments/yang-parameters).";
revision 2026-03-10 {
description
"Initial revision.";
reference
"RFC 9922: A Common YANG Data Model for Scheduling";
}
feature basic-recurrence {
description
"Indicates that the server supports configuring a basic
scheduled recurrence.";
}
feature icalendar-recurrence {
description
"Indicates that the server supports configuring a comprehensive
scheduled iCalendar recurrence.";
reference
"RFC 5545: Internet Calendaring and Scheduling Core Object
Specification (iCalendar),
Sections 3.3.10 and 3.8.5";
}
typedef weekday {
type enumeration {
enum sunday {
value 0;
description
"Sunday of the week.";
}
enum monday {
value 1;
description
"Monday of the week.";
}
enum tuesday {
value 2;
description
"Tuesday of the week.";
}
enum wednesday {
value 3;
description
"Wednesday of the week.";
}
enum thursday {
value 4;
description
"Thursday of the week.";
}
enum friday {
value 5;
description
"Friday of the week.";
}
enum saturday {
value 6;
description
"Saturday of the week.";
}
}
description
"Seven days of the week.";
}
typedef duration {
type string {
pattern '((\+)?|\-)P((([0-9]+)D)?(T(0[0-9]|1[0-9]|2[0-3])'
+ ':[0-5][0-9]:[0-5][0-9]))|P([0-9]+)W';
}
description
"Duration of the time. The format can represent nominal
durations (weeks designated by 'W' and days designated by 'D')
and accurate durations (hours:minutes:seconds follows the
designator 'T').
Note that this value type doesn't support the 'Y' and 'M'
designators to specify durations in terms of years and months.
Negative durations are typically used to schedule an alarm to
trigger before an associated time.";
reference
"RFC 5545: Internet Calendaring and Scheduling Core Object
Specification (iCalendar), Sections 3.3.6 and
3.8.6.3";
}
identity schedule-type {
description
"Base identity for schedule type.";
}
identity one-shot {
base schedule-type;
description
"Indicates a one-shot schedule. That is a schedule that
will trigger an action with the duration being specified as
0 or end time being specified as the same as the start time,
and then the schedule will disable itself.";
}
identity period {
base schedule-type;
description
"Indicates a period-based schedule consisting of either a
start and end or a start and positive duration of time. If
neither an end nor a duration is indicated, the period is
considered to last forever.";
}
identity recurrence {
base schedule-type;
description
"Indicates a recurrence-based schedule.";
}
identity frequency-type {
description
"Base identity for frequency type.";
}
identity secondly {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a second or more.";
}
identity minutely {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a minute or more.";
}
identity hourly {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
an hour or more.";
}
identity daily {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a day or more.";
}
identity weekly {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a week or more.";
}
identity monthly {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a month or more.";
}
identity yearly {
base frequency-type;
description
"Indicates a repeating rule based on an interval of
a year or more.";
}
identity schedule-state {
description
"Base identity for schedule state.";
}
identity enabled {
base schedule-state;
description
"Indicates a schedule with an enabled state.";
}
identity finished {
base schedule-state;
description
"Indicates a schedule with a finished state.
The finished state indicates that the schedule has ended.";
}
identity disabled {
base schedule-state;
description
"Indicates a schedule with a disabled state.";
}
identity out-of-date {
base schedule-state;
description
"Indicates a schedule that is received out-of-date.";
}
identity conflicted {
base schedule-state;
description
"Indicates a schedule with a conflicted state with other
schedules.";
}
identity discard-action-type {
description
"Base identity for the action for the responder to take
when a requested schedule cannot be accepted for any
reason and is discarded.";
}
identity warning {
base discard-action-type;
description
"Indicates that a warning message is generated
when a schedule is discarded.";
}
identity error {
base discard-action-type;
description
"Indicates that an error message is generated
when a schedule is discarded.";
}
identity silently-discard {
base discard-action-type;
description
"Indicates that a schedule that is not valid is silently
discarded.";
}
grouping generic-schedule-params {
description
"Includes a set of generic parameters that are followed by
the entity that supports schedules.
Such parameters are used as guards to prevent, e.g., stale
configuration.";
leaf description {
type string;
description
"Provides a description of the schedule.";
}
leaf time-zone-identifier {
type sys:timezone-name;
description
"Indicates the identifier for the time zone. This parameter
MUST be specified if any of the date and time values are
in the format of local time. It MUST NOT be applied to
date and time values that are specified in the format of
UTC or time zone offset to UTC.";
}
leaf validity {
type yang:date-and-time;
description
"Specifies the date and time after which a schedule will not
be considered as valid. This parameter takes precedence
over similar attributes that are provided at the schedule
instance itself.";
}
leaf max-allowed-start {
type yang:date-and-time;
description
"Specifies the maximum scheduled start date and time.
A requested schedule whose first instance occurs after
this value cannot be accepted by the entity. Specifically,
a requested schedule will be rejected if the first
occurrence of that schedule exceeds 'max-allowed-start'.";
}
leaf min-allowed-start {
type yang:date-and-time;
description
"Specifies the minimum scheduled start date and time.
A requested schedule whose first instance occurs before
this value cannot be accepted by the entity. Specifically,
a requested schedule will be rejected if the first
occurrence of that schedule is scheduled before
'min-allowed-start'.";
}
leaf max-allowed-end {
type yang:date-and-time;
description
"A requested schedule will be rejected if the end time of
the last occurrence exceeds 'max-allowed-end'.";
}
leaf discard-action {
type identityref {
base discard-action-type;
}
description
"Specifies the behavior when a schedule is discarded for
any reason, e.g., failing to satisfy the guards in this
grouping or being received out-of-date.";
}
}
grouping period-of-time {
description
"This grouping is defined for the period of time property.";
reference
"RFC 5545: Internet Calendaring and Scheduling Core Object
Specification (iCalendar), Section 3.3.9";
leaf period-description {
type string;
description
"Provides a description of the period.";
}
leaf period-start {
type yang:date-and-time;
description
"Period start time.";
}
leaf time-zone-identifier {
type sys:timezone-name;
description
"Indicates the identifier for the time zone. This parameter
MUST be specified if either the 'period-start' or
'period-end' value is reported in local time format.
It MUST NOT be applied to date and time values that are
specified in the format of UTC or time zone offset
to UTC.";
}
choice period-type {
description
"Indicates the type of the time period. Two types are
supported. If no choice is indicated, the period is
considered to last forever.";
case explicit {
description
"A period of time is identified by its start and its end.
'period-start' indicates the period start.";
leaf period-end {
type yang:date-and-time;
description
"A period of time is defined by a start and end time.
The start MUST be no later than the end. The period
is considered as a one-shot schedule if the end time
is the same as the start time.";
}
}
case duration {
description
"A period of time is defined by a start and a non-negative
duration of time.";
leaf duration {
type duration {
pattern 'P((([0-9]+)D)?(T(0[0-9]|1[0-9]|2[0-3])'
+ ':[0-5][0-9]:[0-5][0-9]))|P([0-9]+)W';
}
description
"A non-negative duration of time. This value is
equivalent to the format of 'duration' type except that
the value cannot be negative. The period is considered
to be a one-shot schedule if the value is 0.";
}
}
}
}
grouping recurrence-basic {
description
"A simple definition of recurrence.";
leaf recurrence-description {
type string;
description
"Provides a description of the recurrence.";
}
leaf frequency {
type identityref {
base frequency-type;
}
description
"Specifies the frequency type of the recurrence rule.";
}
leaf interval {
type uint32 {
range "1..max";
}
must '../frequency' {
error-message "Frequency must be provided.";
}
description
"A positive integer representing the interval at which the
recurrence rule repeats. For example, within a 'daily'
recurrence rule, a value of '8' means every eight days.";
}
}
grouping recurrence-utc {
description
"A simple definition of recurrence with time specified in
UTC format.";
container recurrence-first {
description
"Specifies the first instance of the recurrence. If
unspecified, the recurrence is considered to start from
the date and time when the recurrence pattern is first
satisfied.";
leaf start-time-utc {
type yang:date-and-time;
description
"Defines the date and time of the first instance
in the recurrence set. A UTC format MUST be used.";
}
leaf duration {
type uint32;
units "seconds";
description
"When specified, it indicates how long the first occurrence
lasts. Unless specified otherwise, it also applies to all
the other instances in the recurrence set.";
}
}
choice recurrence-end {
description
"Modes to control the end of a recurrence rule. If no
choice is indicated, the recurrence rule is considered
to repeat forever.";
case until {
description
"This case defines a way that limits the end of
a recurrence rule in an inclusive manner.";
leaf utc-until {
type yang:date-and-time;
description
"This parameter specifies a date and time value to
inclusively terminate the recurrence in UTC format.
That is, if the value specified by this parameter is
synchronized with the specified recurrence rule, it
becomes the last instance of the recurrence rule.";
}
}
case count {
description
"This case defines the number of occurrences at which
to terminate the recurrence rule.";
leaf count {
type uint32 {
range "1..max";
}
description
"The positive number of occurrences at which to
terminate the recurrence rule.";
}
}
}
uses recurrence-basic;
}
grouping recurrence-with-time-zone {
description
"A simple definition of recurrence to specify a recurrence
rule with a time zone.";
container recurrence-first {
description
"Specifies the first instance of the recurrence. If
unspecified, the recurrence is considered to start from
the date and time when the recurrence pattern is first
satisfied.";
leaf start-time {
type yang:date-and-time;
description
"Defines the date and time of the first instance
in the recurrence set.";
}
leaf duration {
type duration;
description
"When specified, it indicates how long the first
occurrence lasts. Unless specified otherwise, it also
applies to all the other instances in the recurrence
set.";
}
}
leaf time-zone-identifier {
type sys:timezone-name;
description
"Indicates the identifier for the time zone in a time
zone database. This parameter MUST be specified if either
the 'start-time' or 'until' value is reported in local
time format. It MUST NOT be applied to date and time
values that are specified in the format of UTC or time
zone offset to UTC.";
}
choice recurrence-end {
description
"Modes to terminate the recurrence rule. If no choice is
indicated, the recurrence rule is considered to repeat
forever.";
case until {
description
"The end of the recurrence rule is indicated by a specific
date-and-time value in an inclusive manner.";
leaf until {
type yang:date-and-time;
description
"Specifies a date and time value to inclusively terminate
the recurrence. That is, if the value specified by
this parameter is synchronized with the specified
recurrence, it becomes the last instance of the
recurrence.";
}
}
case count {
description
"The end of the recurrence is indicated by the number
of occurrences.";
leaf count {
type uint32 {
range "1..max";
}
description
"The positive number of occurrences at which to
terminate the recurrence.";
}
}
}
uses recurrence-basic;
}
grouping recurrence-utc-with-periods {
description
"This grouping defines an aggregate set of repeating
occurrences with UTC time format. The recurrence instances
are specified by the occurrences defined by both the
recurrence rule and 'period-timeticks' list. Duplicate
instances are ignored.";
uses recurrence-utc;
list period-timeticks {
key "period-start";
description
"A list of periods with timeticks formats.";
leaf period-start {
type yang:timeticks;
must "(not(derived-from-or-self(../../frequency,"
+ "'schedule:secondly')) or (current() < 100)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:minutely')) or (current() < 6000)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:hourly')) or (current() < 360000)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:daily')) or (current() < 8640000)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:weekly')) or (current() < 60480000)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:monthly')) or (current() < 267840000)) and "
+ "(not(derived-from-or-self(../../frequency,"
+ "'schedule:yearly')) or (current() < 3162240000))" {
error-message
"The 'period-start' must not exceed the frequency
interval.";
}
description
"Start time of the schedule within one recurrence.
Given that the value is in timeticks format
(i.e., 1/100 of a second), the values in the must
statement translate to 100 = 1 s (secondly),
6000 = 60 s = 1 min (minutely), and so on for all
instances in the must statement invariant.";
}
leaf period-end {
type yang:timeticks;
description
"End time of the schedule within one recurrence.
The period start MUST be no later than the period
end.";
}
}
}
grouping recurrence-time-zone-with-periods {
description
"This grouping defines an aggregate set of repeating
occurrences with local time format and time zone specified.
The recurrence instances are specified by the occurrences
defined by both the recurrence rule and 'period' list.
Duplicate instances are ignored.";
uses recurrence-with-time-zone;
list period {
key "period-start";
description
"A list of periods with date-and-time formats.";
uses period-of-time;
}
}
grouping icalendar-recurrence {
description
"This grouping specifies properties of a recurrence rule.";
reference
"RFC 5545: Internet Calendaring and Scheduling Core Object
Specification (iCalendar), Section 3.8.5";
uses recurrence-time-zone-with-periods;
leaf-list bysecond {
type uint32 {
range "0..60";
}
description
"Specifies a list of seconds within a minute.";
}
leaf-list byminute {
type uint32 {
range "0..59";
}
description
"Specifies a list of minutes within an hour.";
}
leaf-list byhour {
type uint32 {
range "0..23";
}
description
"Specifies a list of hours of the day.";
}
list byday {
key "weekday";
description
"Specifies a list of days of the week.";
leaf-list direction {
when "derived-from-or-self(../../frequency, "
+ "'schedule:monthly') or "
+ "(derived-from-or-self(../../frequency,"
+ "'schedule:yearly') and not(../../byyearweek))";
type int32 {
range "-53..-1|1..53";
}
description
"When specified, it indicates the nth occurrence of a
specific day within the monthly or yearly recurrence
rule. For example, within a monthly rule, +1 monday
represents the first Monday within the month, whereas
-1 monday represents the last Monday of the month.";
}
leaf weekday {
type schedule:weekday;
description
"Corresponds to seven days of the week.";
}
}
leaf-list bymonthday {
type int32 {
range "-31..-1|1..31";
}
description
"Specifies a list of days of the month.";
}
leaf-list byyearday {
type int32 {
range "-366..-1|1..366";
}
description
"Specifies a list of days of the year.";
}
leaf-list byyearweek {
when "derived-from-or-self(../frequency, 'schedule:yearly')";
type int32 {
range "-53..-1|1..53";
}
description
"Specifies a list of weeks of the year.";
}
leaf-list byyearmonth {
type uint32 {
range "1..12";
}
description
"Specifies a list of months of the year.";
}
leaf-list bysetpos {
type int32 {
range "-366..-1|1..366";
}
description
"Specifies a list of values that corresponds to the nth
occurrence within the set of recurrence instances
specified by the rule. It must only be used in conjunction
with another 'byxxx' (bysecond, byminute, etc.) rule
part.";
}
leaf workweek-start {
type schedule:weekday;
description
"Specifies the day on which the workweek starts.";
}
leaf-list exception-dates {
type yang:date-and-time;
description
"Defines a list of exceptions for recurrence.";
}
}
grouping schedule-status {
description
"This grouping defines common properties of scheduling
status.";
leaf state {
type identityref {
base schedule-state;
}
description
"Indicates the current state of the schedule.";
}
leaf version {
type uint16;
description
"Indicates the version number of the schedule.";
}
leaf schedule-type {
type identityref {
base schedule-type;
}
description
"Indicates the schedule type.";
}
leaf local-time {
type yang:date-and-time;
config false;
description
"Reports the local time as used by the entity that
hosts the schedule.";
}
leaf last-update {
type yang:date-and-time;
config false;
description
"Reports the timestamp of when the schedule is last
updated.";
}
leaf counter {
when "derived-from-or-self(../schedule-type, "
+ "'schedule:recurrence')";
type yang:counter32;
config false;
description
"The number of occurrences while invoking the scheduled
action successfully. The count wraps around when it reaches
the maximum value.";
}
leaf last-occurrence {
when "derived-from-or-self(../schedule-type, "
+ "'schedule:recurrence')";
type yang:date-and-time;
config false;
description
"Indicates the timestamp of last occurrence.";
}
leaf upcoming-occurrence {
when "derived-from-or-self(../schedule-type, "
+ "'schedule:recurrence')"
+ "and derived-from-or-self(../state, 'schedule:enabled')";
type yang:date-and-time;
config false;
description
"Indicates the timestamp of next occurrence.";
}
leaf last-failed-occurrence {
when "derived-from-or-self(../schedule-type, "
+ "'schedule:recurrence')";
type yang:date-and-time;
config false;
description
"Indicates the timestamp of last failed action triggered by
the schedule.";
}
leaf failure-counter {
when "derived-from-or-self(../schedule-type, "
+ "'schedule:recurrence')";
type yang:counter32;
config false;
description
"Counts the number of failures while invoking the scheduled
action.";
}
}
grouping schedule-status-with-time-zone {
description
"This grouping defines common properties of scheduling
status, including timezone.";
leaf time-zone-identifier {
type sys:timezone-name;
config false;
description
"Indicates the identifier for the time zone in a time
zone database.";
}
uses schedule-status;
}
grouping schedule-status-with-name {
description
"This grouping defines common properties of scheduling
status, including a schedule name.";
leaf schedule-name {
type string;
description
"The schedule identifier that uniquely identifies a
schedule within a device, controller, network, etc.
The unicity scope depends on the implementation.";
}
uses schedule-status;
}
}
+187
View File
@@ -0,0 +1,187 @@
module infix-schedule {
yang-version 1.1;
namespace "urn:project:yang:infix-schedule";
prefix infix-schedule;
import ietf-system {
prefix sys;
}
import ietf-schedule {
prefix schedule;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Infix deviations and augments to ietf-schedule";
revision 2026-06-17 {
description
"Initial revision - system scheduling.
Reusable named schedules built on the
ietf-schedule:icalendar-recurrence grouping (RFC 9922), pruned via
deviations to the subset expressible as five-field cron.";
reference "internal";
}
typedef schedule-ref {
type leafref {
path "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:name";
}
description
"References a named schedule by name. A feature owns its trigger by
pointing a leaf of this type at a schedule; the reference is validated,
so a schedule cannot be deleted while something still uses it, and
several features may share a single recurrence.";
}
/*
* Prune ietf-schedule:icalendar-recurrence down to the subset that maps
* cleanly onto a five-field cron expression. Everything removed here has
* no cron equivalent; what survives (frequency, interval, byminute, byhour,
* byday/weekday, bymonthday, byyearmonth) is exactly the surface the
* scheduler backend implements.
*/
/* No start anchor, no UTC/timezone duration -- cron has no notion of one. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:recurrence-first" {
deviate not-supported;
}
/* No way to bound a recurrence by end-time (until) or occurrence count in
* cron; drop the whole recurrence-end choice and both of its cases. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:recurrence-end" {
deviate not-supported;
}
/* Explicit period lists are not expressible as cron fields. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:period" {
deviate not-supported;
}
/* Cron has no seconds field. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:bysecond" {
deviate not-supported;
}
/* "nth weekday of the month" has no five-field cron equivalent. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:byday"
+ "/infix-schedule:direction" {
deviate not-supported;
}
/* Day-of-year, week-of-year, and set-position have no cron field. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:byyearday" {
deviate not-supported;
}
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:byyearweek" {
deviate not-supported;
}
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:bysetpos" {
deviate not-supported;
}
/* Cron has no configurable week-start. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:workweek-start" {
deviate not-supported;
}
/* No exception-date support in cron. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:exception-dates" {
deviate not-supported;
}
/* Schedules run in the system's local time; no per-schedule timezone. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:time-zone-identifier" {
deviate not-supported;
}
/* Negative values (last N days) have no five-field cron equivalent. */
deviation "/sys:system/infix-schedule:schedules/infix-schedule:schedule"
+ "/infix-schedule:recurrence/infix-schedule:bymonthday" {
deviate replace {
type int32 {
range "1..31";
}
}
}
augment "/sys:system" {
description
"Scheduling configuration under ietf-system.";
container schedules {
description
"Container for all configured schedules.";
list schedule {
key "name";
description
"A named, reusable recurrence (time-spec). Schedules carry no
action of their own; features trigger off a schedule by pointing
a schedule-ref leaf at its name.";
leaf name {
type string;
description
"Unique name identifying this schedule.";
}
leaf enabled {
type boolean;
default "true";
description
"Turn this schedule on or off. When off, anything that uses
it stops running, but the schedule itself is kept.";
}
leaf description {
type string;
description
"Optional human-readable description of this schedule's purpose.";
}
must "recurrence" {
error-message "A recurrence rule is required for each schedule.";
}
container recurrence {
if-feature "schedule:icalendar-recurrence";
description
"Recurrence rule controlling when the schedule fires.
Uses the standard iCalendar recurrence grouping; nodes with no
five-field cron equivalent are removed via deviations in this
module.";
uses schedule:icalendar-recurrence {
refine frequency {
mandatory true;
/* secondly has no cron field; a missing frequency would
* otherwise compile to the every-minute expression. */
must "not(derived-from-or-self(., 'schedule:secondly'))" {
error-message
"secondly frequency is not supported; the finest "
+ "supported resolution is minutely.";
}
}
refine interval {
default 1;
}
}
/* cron takes the union of day-of-month and day-of-week, not the
* RFC 5545 intersection, so forbid combining them. */
must "not(bymonthday and byday)" {
error-message
"Combining bymonthday and byday is not supported: cron would "
+ "fire on the union of the two, not their intersection.";
}
}
}
}
}
}
+1
View File
@@ -0,0 +1 @@
infix-schedule.yang
@@ -16,10 +16,18 @@ submodule infix-system-software {
prefix sys;
}
import infix-schedule {
prefix infix-schedule;
}
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Software status and upgrade.";
revision 2026-06-17 {
description "Add check-update config, triggered from a referenced schedule";
reference "Internal";
}
revision 2024-12-16 {
description "Add boot-order operational data";
reference "Internal";
@@ -80,6 +88,45 @@ submodule infix-system-software {
"The last error encountered by the installer service.";
}
}
augment "/sys:system" {
container software {
description
"Software management configuration.";
container check-update {
description
"Policy for automatic software update checks.
When 'enabled' and 'schedule' references a schedule, the system
checks the configured URL for a newer release on each occurrence
and logs a notification if one is found.";
leaf enabled {
type boolean;
default false;
description
"Enable automatic update checks.";
}
leaf schedule {
type infix-schedule:schedule-ref;
description
"The schedule whose occurrences trigger an update check.
Without a referenced schedule no checks are performed.";
}
leaf update-url {
type string;
default "https://github.com/kernelkit/infix";
description
"Base URL of the update source. The check script appends
/releases/latest and follows the redirect to determine the
latest release tag. Override for customer-specific channels.";
}
}
}
}
augment "/sys:system-state" {
container software {
description
+25
View File
@@ -22,12 +22,20 @@ module infix-system {
"RFC 6991: Common YANG Data Types";
}
import infix-schedule {
prefix infix-schedule;
}
include infix-system-software;
organization "KernelKit";
contact "kernelkit@googlegroups.com";
description "Infix augments and deviations to ietf-system.";
revision 2026-06-17 {
description "Add scheduled-reboot, triggered from a referenced schedule.";
reference "internal";
}
revision 2026-03-09 {
description "Add stratumweight to NTP client configuration.
@@ -318,6 +326,23 @@ module infix-system {
}
}
augment "/sys:system" {
description "Scheduled reboot of the system.";
container scheduled-reboot {
description
"Reboot the system on the occurrences of a referenced schedule.
Without a referenced schedule the system is never rebooted on a
schedule.";
leaf schedule {
type infix-schedule:schedule-ref;
description
"The schedule whose occurrences trigger a system reboot.";
}
}
}
augment "/sys:system/sys:ntp" {
leaf stratum-weight {
type decimal64 {
+69 -30
View File
@@ -11,7 +11,9 @@ import (
"math"
"net"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"sync"
@@ -199,15 +201,15 @@ type hardwareWrapper struct {
}
type hwComponentJSON struct {
Name string `json:"name"`
Class string `json:"class"`
Description string `json:"description"`
Parent string `json:"parent"`
MfgName string `json:"mfg-name"`
ModelName string `json:"model-name"`
SerialNum string `json:"serial-num"`
HardwareRev string `json:"hardware-rev"`
PhysAddress string `json:"infix-hardware:phys-address"`
Name string `json:"name"`
Class string `json:"class"`
Description string `json:"description"`
Parent string `json:"parent"`
MfgName string `json:"mfg-name"`
ModelName string `json:"model-name"`
SerialNum string `json:"serial-num"`
HardwareRev string `json:"hardware-rev"`
PhysAddress string `json:"infix-hardware:phys-address"`
WiFiRadio *wifiRadioHWJSON `json:"infix-hardware:wifi-radio"`
GPSReceiver *struct{} `json:"infix-hardware:gps-receiver"`
SensorData *struct {
@@ -226,26 +228,26 @@ type hwComponentJSON struct {
type dashboardData struct {
PageData
Hostname string
Contact string
Location string
OSName string
OSVersion string
Machine string
CurrentTime string
Software string
Uptime string
MemTotal int64
MemUsed int64
MemPercent int
MemClass string
Load1 string
Load5 string
Load15 string
CPUClass string
Disks []diskEntry
Board boardInfo
KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory.
Hostname string
Contact string
Location string
OSName string
OSVersion string
Machine string
CurrentTime string
Software string
Uptime string
MemTotal int64
MemUsed int64
MemPercent int
MemClass string
Load1 string
Load5 string
Load15 string
CPUClass string
Disks []diskEntry
Board boardInfo
KeyVitals []sensorEntry // Overview's at-a-glance subset: CPU/SoC + wifi-radio temperatures and fan RPMs. Status > Hardware has the full inventory.
// Connectivity card.
Gateways []gatewayEntry
InternetProbe string // address pinged for the Internet reachability row
@@ -254,7 +256,11 @@ type dashboardData struct {
NTPSync string // "" / the selected NTP source address
// Addresses card.
Addresses []ifaceAddrEntry
Error string
// Software-update banner — shown only when an update is available.
UpdateAvailable bool
UpdateMessage string // verbatim CLI/login-banner notice
UpdateURL string // release URL extracted from the notice
Error string
}
// gatewayEntry is a default route's next-hop.
@@ -305,6 +311,31 @@ type diskEntry struct {
// reachability row — a well-known, stable anycast resolver.
const internetProbe = "1.1.1.1"
// updateNoticeFile holds the software-update notice written by
// /usr/sbin/check-update — the same text the CLI shows at login.
// A package var so tests can point it elsewhere.
var updateNoticeFile = "/run/os-update"
var updateURLRe = regexp.MustCompile(`https?://\S+`)
// readUpdateNotice returns the software-update message verbatim (as shown in
// the CLI login banner) and the release URL extracted from it. Both are empty
// when no update is pending or the notice file is absent/empty.
func readUpdateNotice() (msg, url string) {
b, err := os.ReadFile(updateNoticeFile)
if err != nil {
return "", ""
}
msg = strings.TrimSpace(string(b))
if msg == "" {
return "", ""
}
if u := updateURLRe.FindString(msg); u != "" {
url = strings.TrimRight(u, ").,;")
}
return msg, url
}
// DashboardHandler serves the main dashboard page.
type DashboardHandler struct {
Template *template.Template
@@ -501,6 +532,14 @@ func (h *DashboardHandler) Index(w http.ResponseWriter, r *http.Request) {
}
data.Addresses = ifaceAddresses(ifaces)
// Software-update banner: surfaces the same notice as the CLI login
// banner (written by check-update to the notice file).
if msg, url := readUpdateNotice(); msg != "" {
data.UpdateAvailable = true
data.UpdateMessage = msg
data.UpdateURL = url
}
tmplName := "dashboard.html"
if r.Header.Get("HX-Request") == "true" {
tmplName = "content"
+37
View File
@@ -579,6 +579,43 @@ details[open].nav-group-top > summary.nav-group-summary-top::before {
padding: 1.25rem;
}
/* Dashboard software-update banner — amber "attention" treatment, bright
and theme-aware, but built from the same card primitives as everything
else. Spans the full info-grid width and sits at the top. */
.update-card {
border-color: var(--warning-border);
background: var(--warning-bg);
box-shadow: 0 0 0 1px var(--warning-border), var(--shadow-sm);
}
.update-card .update-card-header {
background: var(--warning);
color: #fff;
border-bottom-color: var(--warning-border);
display: flex;
align-items: center;
gap: 0.5rem;
}
.update-card .update-card-icon {
font-size: 1rem;
line-height: 1;
}
.update-card .update-card-body {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem 1.5rem;
flex-wrap: wrap;
}
.update-card .update-card-msg {
margin: 0;
color: var(--warning-fg);
font-size: 0.95rem;
font-weight: 500;
}
.update-card .update-card-action {
flex: none;
}
/* ==========================================================================
Tables
+15
View File
@@ -8,6 +8,21 @@
{{end}}
<section class="info-grid">
{{if .UpdateAvailable}}
<div class="info-card info-grid-full update-card">
<div class="card-header update-card-header">
<span class="update-card-icon" aria-hidden="true">&#x2B06;</span> Software Update Available
</div>
<div class="card-body update-card-body">
<p class="update-card-msg">{{.UpdateMessage}}</p>
{{if .UpdateURL}}
<a href="{{.UpdateURL}}" target="_blank" rel="noopener noreferrer"
class="btn btn-primary update-card-action">View release &rarr;</a>
{{end}}
</div>
</div>
{{end}}
<div class="info-card">
<div class="card-header">System Information</div>
<div class="card-body" style="padding:0">
+3
View File
@@ -22,3 +22,6 @@
- name: System Upgrade
case: upgrade/test.py
- name: Schedule Reboot
case: schedule_reboot/test.py
+1
View File
@@ -0,0 +1 @@
test.adoc
@@ -0,0 +1,21 @@
=== Schedule Reboot
ifdef::topdoc[:imagesdir: {topdoc}../../test/case/system/schedule_reboot]
==== Description
Verify that it is possible to schedule a system reboot using the
infix-schedule module.
==== Topology
image::topology.svg[Schedule Reboot topology, align=center, scaledwidth=75%]
==== Sequence
. Set up topology and attach to target DUT
. Schedule a reboot
. Wait for reboot
. Verify system is back up
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Schedule Reboot
Verify that it is possible to schedule a system reboot using the
infix-schedule module.
"""
import infamy
from infamy.util import wait_boot
with infamy.Test() as test:
with test.step("Set up topology and attach to target DUT"):
env = infamy.Env()
target = env.attach("target", "mgmt", "netconf")
with test.step("Schedule a reboot"):
target.put_config_dicts({
"ietf-system": {
"system": {
"infix-schedule:schedules": {
"schedule": [
{
"name": "reboot-test",
"enabled": True,
"recurrence": {
"frequency": "ietf-schedule:minutely",
"interval": 1
}
}
]
},
"infix-system:scheduled-reboot": {
"schedule": "reboot-test"
}
}
}
})
with test.step("Wait for reboot"):
if not wait_boot(target, env):
test.fail("System did not reboot as expected")
with test.step("Verify system is back up"):
target = env.attach("target", "mgmt", "netconf")
test.succeed()
@@ -0,0 +1,23 @@
graph "1x1" {
layout="neato";
overlap="false";
esep="+80";
node [shape=record, fontname="DejaVu Sans Mono, Book"];
edge [color="cornflowerblue", penwidth="2", fontname="DejaVu Serif, Book"];
host [
label="host | { <mgmt> mgmt }",
pos="0,12!",
requires="controller",
];
target [
label="{ <mgmt> mgmt } | target",
pos="10,12!",
requires="infix",
];
host:mgmt -- target:mgmt [requires="mgmt", color="lightgray"]
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Title: 1x1 Pages: 1 -->
<svg width="424pt" height="45pt"
viewBox="0.00 0.00 424.03 45.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 41)">
<title>1x1</title>
<polygon fill="white" stroke="transparent" points="-4,4 -4,-41 420.03,-41 420.03,4 -4,4"/>
<!-- host -->
<g id="node1" class="node">
<title>host</title>
<polygon fill="none" stroke="black" points="0,-0.5 0,-36.5 100,-36.5 100,-0.5 0,-0.5"/>
<text text-anchor="middle" x="25" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">host</text>
<polyline fill="none" stroke="black" points="50,-0.5 50,-36.5 "/>
<text text-anchor="middle" x="75" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
</g>
<!-- target -->
<g id="node2" class="node">
<title>target</title>
<polygon fill="none" stroke="black" points="300.03,-0.5 300.03,-36.5 416.03,-36.5 416.03,-0.5 300.03,-0.5"/>
<text text-anchor="middle" x="325.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">mgmt</text>
<polyline fill="none" stroke="black" points="350.03,-0.5 350.03,-36.5 "/>
<text text-anchor="middle" x="383.03" y="-14.8" font-family="DejaVu Sans Mono, Book" font-size="14.00">target</text>
</g>
<!-- host&#45;&#45;target -->
<g id="edge1" class="edge">
<title>host:mgmt&#45;&#45;target:mgmt</title>
<path fill="none" stroke="lightgray" stroke-width="2" d="M100,-18.5C100,-18.5 300.03,-18.5 300.03,-18.5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB