mirror of
https://github.com/kernelkit/infix.git
synced 2026-07-22 17:23:00 +02:00
Merge pull request #1523 from saba8814/feat/add-scheduler-support
Add support for ietf-schedule
This commit is contained in:
@@ -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
|
||||
Executable
+55
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# Cron daemon for infix-schedule
|
||||
service [2345] crond -f -- Cron daemon
|
||||
@@ -50,6 +50,7 @@ confd_plugin_la_SOURCES = \
|
||||
if-wireguard.c \
|
||||
keystore.c \
|
||||
system.c \
|
||||
schedule.c \
|
||||
ntp.c \
|
||||
ptp.c \
|
||||
syslog.c \
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">⬆</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 →</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="info-card">
|
||||
<div class="card-header">System Information</div>
|
||||
<div class="card-body" style="padding:0">
|
||||
|
||||
@@ -22,3 +22,6 @@
|
||||
|
||||
- name: System Upgrade
|
||||
case: upgrade/test.py
|
||||
|
||||
- name: Schedule Reboot
|
||||
case: schedule_reboot/test.py
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+45
@@ -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--target -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>host:mgmt--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 |
Reference in New Issue
Block a user