From 60fdf896379cb88e258339a94bebc476d6fda6aa Mon Sep 17 00:00:00 2001 From: Leonardo David Monteiro Date: Tue, 4 Mar 2025 17:21:00 +0100 Subject: [PATCH 1/7] introduce mod/notification-gotify for sending notifications via Gotify (https://gotify.net). --- README.md | 1 + doc/mod/notification-email.md | 1 + doc/mod/notification-gotify.d/appsetup.avif | Bin 0 -> 13928 bytes doc/mod/notification-gotify.md | 90 ++++++++++++ doc/mod/notification-matrix.md | 1 + doc/mod/notification-ntfy.md | 1 + doc/mod/notification-telegram.md | 1 + global-config.rsc | 6 + mod/notification-gotify.rsc | 143 ++++++++++++++++++++ 9 files changed, 244 insertions(+) create mode 100644 doc/mod/notification-gotify.d/appsetup.avif create mode 100644 doc/mod/notification-gotify.md create mode 100644 mod/notification-gotify.rsc diff --git a/README.md b/README.md index fae69869..fe9d148d 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ Available modules * [Inspect variables](doc/mod/inspectvar.md) * [IP address calculation](doc/mod/ipcalc.md) * [Send notifications via e-mail](doc/mod/notification-email.md) +* [Send notifications via Gotify](doc/mod/notification-gotify.md) * [Send notifications via Matrix](doc/mod/notification-matrix.md) * [Send notifications via Ntfy](doc/mod/notification-ntfy.md) * [Send notifications via Telegram](doc/mod/notification-telegram.md) diff --git a/doc/mod/notification-email.md b/doc/mod/notification-email.md index 34d1c09e..7dd9a5e7 100644 --- a/doc/mod/notification-email.md +++ b/doc/mod/notification-email.md @@ -79,6 +79,7 @@ function available: See also -------- +* [Send notifications via Gotify](notification-gotify.md) * [Send notifications via Matrix](notification-matrix.md) * [Send notifications via Ntfy](notification-ntfy.md) * [Send notifications via Telegram](notification-telegram.md) diff --git a/doc/mod/notification-gotify.d/appsetup.avif b/doc/mod/notification-gotify.d/appsetup.avif new file mode 100644 index 0000000000000000000000000000000000000000..55fccd01d252f60fef2a4d84df72ded7eef08c1a GIT binary patch literal 13928 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 literal 0 HcmV?d00001 diff --git a/doc/mod/notification-gotify.md b/doc/mod/notification-gotify.md new file mode 100644 index 00000000..6f617672 --- /dev/null +++ b/doc/mod/notification-gotify.md @@ -0,0 +1,90 @@ +Send notifications via Gotify +=========================== + +[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network) +[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers) +[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.15-yellow?style=flat)](https://mikrotik.com/download/changelogs/) +[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts) +[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J) + +[⬅️ Go back to main README](../../README.md) + +> ℹ️️ **Info**: This module can not be used on its own but requires the base +> installation. See [main README](../../README.md) for details. + +Description +----------- + +This module adds support for sending notifications via +[Gotify](https://gotify.net/). A queue is used to make sure +notifications are not lost on failure but sent later. + +Requirements and installation +----------------------------- + +Just install the module: + + $ScriptInstallUpdate mod/notification-gotify; + +Also deploy the [Gotify server](https://github.com/gotify/server) and optionally install a Gotify client on your mobile device. + +Configuration +------------- + +Follow the [installation instructions](https://gotify.net/docs/install) and the [First Login setup](https://gotify.net/docs/first-login). Once you have a user and account you can start creating apps. Each app is an independent notification feed for a device or application. + +![Create new app](notification-gotify.d/appsetup.avif) + +On creation apps are assigned a Token for authentification and you can also assign a default priority for each app. + +Edit `global-config-overlay`, add `GotifyServer` with your server address and `GotifyToken` with the token from your configured app on the Gotify server. +Then reload the configuration. + +> ℹ️ **Info**: Copy relevant configuration from +> [`global-config`](../../global-config.rsc) (the one without `-overlay`) to +> your local `global-config-overlay` and modify it to your specific needs. + +For a custom service installing an additional certificate may be required. +You may want to install that certificate manually, after finding the +[certificate name from browser](../../CERTIFICATES.md). + +Usage and invocation +-------------------- + +There's nothing special to do. Every script or function sending a notification +will now send it to your Gotify application feed. + +But of course you can use the function to send notifications directly. Give +it a try: + + $SendGotify "Subject..." "Body..."; + +Alternatively this sends a notification with all available and configured +methods: + + $SendNotification "Subject..." "Body..."; + +To use the functions in your own scripts you have to declare them first. +Place this before you call them: + + :global SendGotify; + :global SendNotification; + +In case there is a situation when the queue needs to be purged there is a +function available: + + $PurgeGotifyQueue; + +See also +-------- + +* [Certificate name from browser](../../CERTIFICATES.md) +* [Send notifications via e-mail](notification-email.md) +* [Send notifications via Matrix](notification-matrix.md) +* [Send notifications via Ntfy](notification-ntfy.md) +* [Send notifications via Telegram](notification-telegram.md) + +--- +[⬅️ Go back to main README](../../README.md) +[⬆️ Go back to top](#top) diff --git a/doc/mod/notification-matrix.md b/doc/mod/notification-matrix.md index 89c1b01d..b3df374a 100644 --- a/doc/mod/notification-matrix.md +++ b/doc/mod/notification-matrix.md @@ -131,6 +131,7 @@ See also * [Certificate name from browser](../../CERTIFICATES.md) * [Send notifications via e-mail](notification-email.md) +* [Send notifications via Gotify](notification-gotify.md) * [Send notifications via Ntfy](notification-ntfy.md) * [Send notifications via Telegram](notification-telegram.md) diff --git a/doc/mod/notification-ntfy.md b/doc/mod/notification-ntfy.md index 51756ace..cfef7bbc 100644 --- a/doc/mod/notification-ntfy.md +++ b/doc/mod/notification-ntfy.md @@ -90,6 +90,7 @@ See also * [Certificate name from browser](../../CERTIFICATES.md) * [Send notifications via e-mail](notification-email.md) +* [Send notifications via Gotify](notification-gotify.md) * [Send notifications via Matrix](notification-matrix.md) * [Send notifications via Telegram](notification-telegram.md) diff --git a/doc/mod/notification-telegram.md b/doc/mod/notification-telegram.md index 2d00116a..25585a6f 100644 --- a/doc/mod/notification-telegram.md +++ b/doc/mod/notification-telegram.md @@ -115,6 +115,7 @@ See also * [Chat with your router and send commands via Telegram bot](../telegram-chat.md) * [Send notifications via e-mail](notification-email.md) +* [Send notifications via Gotify](notification-gotify.md) * [Send notifications via Matrix](notification-matrix.md) * [Send notifications via Ntfy](notification-ntfy.md) diff --git a/global-config.rsc b/global-config.rsc index 2524dedc..83f823a0 100644 --- a/global-config.rsc +++ b/global-config.rsc @@ -63,6 +63,12 @@ :global NtfyServerToken ""; :global NtfyTopic ""; +# You can send Gotify notifications. Configure these settings and +# install the module: +# $ScriptInstallUpdate mod/notification-gotify +:global GotifyServer ""; +:global GotifyToken ""; + # It is possible to override e-mail, Telegram, Matrix and Ntfy setting # for every script. This is done in arrays, where 'Override' is appended # to the variable name, like this: diff --git a/mod/notification-gotify.rsc b/mod/notification-gotify.rsc new file mode 100644 index 00000000..9925c9f5 --- /dev/null +++ b/mod/notification-gotify.rsc @@ -0,0 +1,143 @@ +#!rsc by RouterOS +# RouterOS script: mod/notification-gotify +# Copyright (c) 2025 Christian Hesse +# Leonardo David Monteiro +# https://rsc.eworm.de/COPYING.md +# +# requires RouterOS, version=7.15 +# requires device-mode, fetch, scheduler +# +# send notifications via Gotify +# https://rsc.eworm.de/doc/mod/notification-ntfy.md + +:global FlushGotifyQueue; +:global NotificationFunctions; +:global PurgeGotifyQueue; +:global SendGotify; +:global SendGotify2; + +# flush Gotify queue +:set FlushGotifyQueue do={ + :do { + :global GotifyQueue; + :global IsFullyConnected; + :global LogPrint; + + :if ([ $IsFullyConnected ] = false) do={ + $LogPrint debug $0 ("System is not fully connected, not flushing."); + :return false; + } + + :local AllDone true; + :local QueueLen [ :len $GotifyQueue ]; + + :if ([ :len [ /system/scheduler/find where name="_FlushGotifyQueue" ] ] > 0 && $QueueLen = 0) do={ + $LogPrint warning $0 ("Flushing Gotify messages from scheduler, but queue is empty."); + } + + :foreach Id,Message in=$GotifyQueue do={ + :if ([ :typeof $Message ] = "array" ) do={ + :do { + /tool/fetch check-certificate=yes-without-crl output=none http-method=post \ + http-header-field=($Message->"headers") http-data=[ :serialize to=json ($Message->"message") ] \ + ($Message->"url") as-value; + :set ($GotifyQueue->$Id); + } on-error={ + $LogPrint debug $0 ("Sending queued Gotify message failed."); + :set AllDone false; + } + } + } + + :if ($AllDone = true && $QueueLen = [ :len $GotifyQueue ]) do={ + /system/scheduler/remove [ find where name="_FlushGotifyQueue" ]; + :set GotifyQueue; + } + } on-error={ + :global ExitError; $ExitError false $0; + } +} + +# send notification via Gotify - expects one array argument +:set ($NotificationFunctions->"gotify") do={ + :local Notification $1; + + :global Identity; + :global IdentityExtra; + :global GotifyQueue; + :global GotifyServer; + :global GotifyServerOverride; + :global GotifyToken; + :global GotifyTokenOverride; + + :global EitherOr; + :global LogPrint; + :global SymbolForNotification; + + :local Server [ $EitherOr ($GotifyServerOverride->($Notification->"origin")) $GotifyServer ]; + :local Token [ $EitherOr ($GotifyTokenOverride->($Notification->"origin")) $GotifyToken ]; + + :if ([ :len $Token ] = 0) do={ + :return false; + } + :local Url ("https://" . $Server . "/message"); + + :local Headers ({ ("X-Gotify-Key: " . $Token); "Content-Type: application/json" }); + + :local Message; + :if ([ :len [ :tostr ($Notification->"priority") ] ] > 0) do={ + :set Message ({"title"="[" . $IdentityExtra . $Identity . "] " . ($Notification->"subject"); + "message"=($Notification->"message"); + "priority"=$Priority }); + } else={ + :set Message ({"title"="[" . $IdentityExtra . $Identity . "] " . ($Notification->"subject"); + "message"=($Notification->"message")}); + } + + :do { + /tool/fetch check-certificate=yes-without-crl output=none http-method=post \ + http-header-field=$Headers http-data=[:serialize to=json $Message] $Url as-value; + } on-error={ + $LogPrint info $0 ("Failed sending Gotify notification! Queuing..."); + + :if ([ :typeof $GotifyQueue ] = "nothing") do={ + :set GotifyQueue ({}); + } + :set ($Message->"title") ("[Queued] " . ($Notification->"subject")); + :set ($Message->"message") (($Notification->"message") . "\n[This message was queued at " . \ + [ /system/clock/get date ] . " " . [ /system/clock/get time ] . "]"); + :set ($GotifyQueue->[ :len $GotifyQueue ]) { url=$Url; headers=$Headers; message=$Message }; + :if ([ :len [ /system/scheduler/find where name="_FlushGotifyQueue" ] ] = 0) do={ + /system/scheduler/add name="_FlushGotifyQueue" interval=1m start-time=startup \ + on-event=(":global FlushGotifyQueue; \$FlushGotifyQueue;"); + } + } +} + +# purge the Gotify queue +:set PurgeGotifyQueue do={ + :global GotifyQueue; + + /system/scheduler/remove [ find where name="_FlushGotifyQueue" ]; + :set GotifyQueue; +} + +# send notification via Gotify - expects at least two string arguments +:set SendGotify do={ + :do { + :global SendGotify2; + + $SendGotify2 ({ origin=$0; subject=$1; message=$2; priority=$3 }); + } on-error={ + :global ExitError; $ExitError false $0; + } +} + +# send notification via Gotify - expects one array argument +:set SendGotify2 do={ + :local Notification $1; + + :global NotificationFunctions; + + ($NotificationFunctions->"gotify") ("\$NotificationFunctions->\"gotify\"") $Notification; +} From f8c3659f6a329498b7aea5ff7f4b67ae1ef727df Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Wed, 26 Mar 2025 09:19:02 +0100 Subject: [PATCH 2/7] support creating the checksums file --- .gitignore | 3 +++ Makefile | 7 +++++-- contrib/checksums.sh | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100755 contrib/checksums.sh diff --git a/.gitignore b/.gitignore index cf89f870..8abdc284 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,8 @@ # html files (as generated from markdown) *.html +# checksums file as used by $ScriptInstallUpdate +checksums.json + # Mac OS X folder settings file .DS_Store diff --git a/Makefile b/Makefile index d21713c2..89517410 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ WIFI = $(wildcard *.wifi.rsc) MARKDOWN = $(wildcard *.md doc/*.md doc/mod/*.md) HTML = $(MARKDOWN:.md=.html) -all: $(CAPSMAN) $(LOCAL) $(WIFI) $(HTML) +all: $(CAPSMAN) $(LOCAL) $(WIFI) $(HTML) checksums.json %.html: %.md Makefile markdown $< | sed 's/href="\([-_\./[:alnum:]]*\)\.md"/href="\1.html"/g' > $@ @@ -32,5 +32,8 @@ all: $(CAPSMAN) $(LOCAL) $(WIFI) $(HTML) -e '/^# !!/,/^# !!/c # !! Do not edit this file, it is generated from template!' \ < $< > $@ +checksums.json: contrib/checksums.sh *.rsc */*.rsc + contrib/checksums.sh + clean: - rm -f $(HTML) + rm -f $(HTML) checksums.json diff --git a/contrib/checksums.sh b/contrib/checksums.sh new file mode 100755 index 00000000..b472b49f --- /dev/null +++ b/contrib/checksums.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# generate a checksums file as used by $ScriptInstallUpdate + +set -e + +md5sum $(find -name '*.rsc' | sort) | \ + sed -e "s| \./||" -e 's|.rsc$||' | \ + jq --raw-input --null-input '[ inputs | split (" ") | { (.[1]): (.[0]) }] | add' > 'checksums.json' From 690951469291ae96ead1f88226be78ec7735d098 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Thu, 20 Mar 2025 09:48:01 +0100 Subject: [PATCH 3/7] check-routeros-update: send certificate warning just once... ... and another one once expired. --- check-routeros-update.rsc | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/check-routeros-update.rsc b/check-routeros-update.rsc index 78161e4b..51df175d 100644 --- a/check-routeros-update.rsc +++ b/check-routeros-update.rsc @@ -22,6 +22,7 @@ :global SafeUpdateNeighborIdentity; :global SafeUpdatePatch; :global SafeUpdateUrl; + :global SentCertificateNotification; :global SentRouterosUpdateNotification; :global DeviceInfo; @@ -70,20 +71,26 @@ :if ([ :typeof ($License->"deadline-at") ] = "str") do={ :if ([ :len ($License->"next-renewal-at") ] = 0 && ($License->"limited-upgrades") = true) do={ $LogPrint warning $ScriptName ("Your license expired on " . ($License->"deadline-at") . "!"); - $SendNotification2 ({ origin=$ScriptName; \ - subject=([ $SymbolForNotification "warning-sign" ] . "License expired!"); \ - message=("Your license expired on " . ($License->"deadline-at") . \ - ", can no longer update RouterOS on " . $Identity . "...") }); + :if ($SentCertificateNotification != "expired") do={ + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "warning-sign" ] . "License expired!"); \ + message=("Your license expired on " . ($License->"deadline-at") . \ + ", can no longer update RouterOS on " . $Identity . "...") }); + :set SentCertificateNotification "expired"; + } :set ExitOK true; :error false; } :if ([ :totime ($License->"deadline-at") ] - 3w < [ :timestamp ]) do={ $LogPrint warning $ScriptName ("Your license will expire on " . ($License->"deadline-at") . "!"); - $SendNotification2 ({ origin=$ScriptName; \ - subject=([ $SymbolForNotification "warning-sign" ] . "License about to expire!"); \ - message=("Your license failed to renew and is about to expire on " . \ - ($License->"deadline-at") . " on " . $Identity . "...") }); + :if ($SentCertificateNotification != "warning") do={ + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "warning-sign" ] . "License about to expire!"); \ + message=("Your license failed to renew and is about to expire on " . \ + ($License->"deadline-at") . " on " . $Identity . "...") }); + :set SentCertificateNotification "warning"; + } } } From 16c9ce437ef268780303f627bba08a166e2aef05 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Mon, 24 Mar 2025 21:14:12 +0100 Subject: [PATCH 4/7] check-routeros-update: send notification on renewed license... ... when a warning has been sent before. --- check-routeros-update.rsc | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/check-routeros-update.rsc b/check-routeros-update.rsc index 51df175d..1a5a930f 100644 --- a/check-routeros-update.rsc +++ b/check-routeros-update.rsc @@ -92,6 +92,16 @@ :set SentCertificateNotification "warning"; } } + + :if ([ :typeof $SentCertificateNotification ] = "str" && \ + [ :totime ($License->"deadline-at") ] - 4w > [ :timestamp ]) do={ + $LogPrint info $ScriptName ("Your license was successfully renewed."); + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "License renewed"); \ + message=("Your license was successfully renewed on " . $Identity . \ + ". It is now valid until " . ($License->"deadline-at") . ".") }); + :set SentCertificateNotification; + } } $LogPrint debug $ScriptName ("Checking for updates..."); From 3c30276e235b6c8105f44db49ba6586d296f264c Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Tue, 25 Mar 2025 17:54:09 +0100 Subject: [PATCH 5/7] check-routeros-update: split off check-perpetual-license... ... and also add documentation, screenshot, etc. --- README.md | 1 + check-perpetual-license.rsc | 78 ++++++++++++++++++ check-routeros-update.rsc | 38 --------- .../notification.avif | Bin 0 -> 4004 bytes doc/check-perpetual-license.md | 70 ++++++++++++++++ doc/check-routeros-update.md | 1 + global-functions.rsc | 2 +- news-and-changes.rsc | 1 + 8 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 check-perpetual-license.rsc create mode 100644 doc/check-perpetual-license.d/notification.avif create mode 100644 doc/check-perpetual-license.md diff --git a/README.md b/README.md index fae69869..ffbc27ed 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ Available scripts * [Renew certificates and notify on expiration](doc/check-certificates.md) * [Notify about health state](doc/check-health.md) * [Notify on LTE firmware upgrade](doc/check-lte-firmware-upgrade.md) +* [Check perpetual license on CHR](doc/check-perpetual-license.md) * [Notify on RouterOS update](doc/check-routeros-update.md) * [Collect MAC addresses in wireless access list](doc/collect-wireless-mac.md) * [Use wireless network with daily psk](doc/daily-psk.md) diff --git a/check-perpetual-license.rsc b/check-perpetual-license.rsc new file mode 100644 index 00000000..9a3a3c74 --- /dev/null +++ b/check-perpetual-license.rsc @@ -0,0 +1,78 @@ +#!rsc by RouterOS +# RouterOS script: check-perpetual-license +# Copyright (c) 2025 Christian Hesse +# https://rsc.eworm.de/COPYING.md +# +# requires RouterOS, version=7.15 +# +# check perpetual license on CHR +# https://rsc.eworm.de/doc/check-perpetual-license.md + +:global GlobalFunctionsReady; +:while ($GlobalFunctionsReady != true) do={ :delay 500ms; } + +:local ExitOK false; +:do { + :local ScriptName [ :jobname ]; + + :global Identity; + :global SentCertificateNotification; + + :global LogPrint; + :global ScriptLock; + :global SendNotification2; + :global SymbolForNotification; + :global WaitFullyConnected; + + :if ([ $ScriptLock $ScriptName ] = false) do={ + :set ExitOK true; + :error false; + } + + $WaitFullyConnected; + + :local License [ /system/license/get ]; + :if ([ :typeof ($License->"deadline-at") ] != "str") do={ + $LogPrint info $ScriptName ("This device does not have a perpetual license."); + :set ExitOK true; + :error true; + } + + :if ([ :len ($License->"next-renewal-at") ] = 0 && ($License->"limited-upgrades") = true) do={ + $LogPrint warning $ScriptName ("Your license expired on " . ($License->"deadline-at") . "!"); + :if ($SentCertificateNotification != "expired") do={ + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "warning-sign" ] . "License expired!"); \ + message=("Your license expired on " . ($License->"deadline-at") . \ + ", can no longer update RouterOS on " . $Identity . "...") }); + :set SentCertificateNotification "expired"; + } + :set ExitOK true; + :error true; + } + + :if ([ :totime ($License->"deadline-at") ] - 3w < [ :timestamp ]) do={ + $LogPrint warning $ScriptName ("Your license will expire on " . ($License->"deadline-at") . "!"); + :if ($SentCertificateNotification != "warning") do={ + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "warning-sign" ] . "License about to expire!"); \ + message=("Your license failed to renew and is about to expire on " . \ + ($License->"deadline-at") . " on " . $Identity . "...") }); + :set SentCertificateNotification "warning"; + } + :set ExitOK true; + :error true; + } + + :if ([ :typeof $SentCertificateNotification ] = "str" && \ + [ :totime ($License->"deadline-at") ] - 4w > [ :timestamp ]) do={ + $LogPrint info $ScriptName ("Your license was successfully renewed."); + $SendNotification2 ({ origin=$ScriptName; \ + subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "License renewed"); \ + message=("Your license was successfully renewed on " . $Identity . \ + ". It is now valid until " . ($License->"deadline-at") . ".") }); + :set SentCertificateNotification; + } +} on-error={ + :global ExitError; $ExitError $ExitOK [ :jobname ]; +} diff --git a/check-routeros-update.rsc b/check-routeros-update.rsc index 1a5a930f..361be34e 100644 --- a/check-routeros-update.rsc +++ b/check-routeros-update.rsc @@ -22,7 +22,6 @@ :global SafeUpdateNeighborIdentity; :global SafeUpdatePatch; :global SafeUpdateUrl; - :global SentCertificateNotification; :global SentRouterosUpdateNotification; :global DeviceInfo; @@ -67,43 +66,6 @@ :error "A reboot for update is already scheduled."; } - :local License [ /system/license/get ]; - :if ([ :typeof ($License->"deadline-at") ] = "str") do={ - :if ([ :len ($License->"next-renewal-at") ] = 0 && ($License->"limited-upgrades") = true) do={ - $LogPrint warning $ScriptName ("Your license expired on " . ($License->"deadline-at") . "!"); - :if ($SentCertificateNotification != "expired") do={ - $SendNotification2 ({ origin=$ScriptName; \ - subject=([ $SymbolForNotification "warning-sign" ] . "License expired!"); \ - message=("Your license expired on " . ($License->"deadline-at") . \ - ", can no longer update RouterOS on " . $Identity . "...") }); - :set SentCertificateNotification "expired"; - } - :set ExitOK true; - :error false; - } - - :if ([ :totime ($License->"deadline-at") ] - 3w < [ :timestamp ]) do={ - $LogPrint warning $ScriptName ("Your license will expire on " . ($License->"deadline-at") . "!"); - :if ($SentCertificateNotification != "warning") do={ - $SendNotification2 ({ origin=$ScriptName; \ - subject=([ $SymbolForNotification "warning-sign" ] . "License about to expire!"); \ - message=("Your license failed to renew and is about to expire on " . \ - ($License->"deadline-at") . " on " . $Identity . "...") }); - :set SentCertificateNotification "warning"; - } - } - - :if ([ :typeof $SentCertificateNotification ] = "str" && \ - [ :totime ($License->"deadline-at") ] - 4w > [ :timestamp ]) do={ - $LogPrint info $ScriptName ("Your license was successfully renewed."); - $SendNotification2 ({ origin=$ScriptName; \ - subject=([ $SymbolForNotification "white-heavy-check-mark" ] . "License renewed"); \ - message=("Your license was successfully renewed on " . $Identity . \ - ". It is now valid until " . ($License->"deadline-at") . ".") }); - :set SentCertificateNotification; - } - } - $LogPrint debug $ScriptName ("Checking for updates..."); /system/package/update/check-for-updates without-paging as-value; :local Update [ /system/package/update/get ]; diff --git a/doc/check-perpetual-license.d/notification.avif b/doc/check-perpetual-license.d/notification.avif new file mode 100644 index 0000000000000000000000000000000000000000..70ca603d4488773dd8a476ced8c89506407b95ef GIT binary patch literal 4004 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 literal 0 HcmV?d00001 diff --git a/doc/check-perpetual-license.md b/doc/check-perpetual-license.md new file mode 100644 index 00000000..d4440045 --- /dev/null +++ b/doc/check-perpetual-license.md @@ -0,0 +1,70 @@ +Check perpetual license on CHR +============================== + +[![GitHub stars](https://img.shields.io/github/stars/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=red)](https://github.com/eworm-de/routeros-scripts/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=green)](https://github.com/eworm-de/routeros-scripts/network) +[![GitHub watchers](https://img.shields.io/github/watchers/eworm-de/routeros-scripts?logo=GitHub&style=flat&color=blue)](https://github.com/eworm-de/routeros-scripts/watchers) +[![required RouterOS version](https://img.shields.io/badge/RouterOS-7.15-yellow?style=flat)](https://mikrotik.com/download/changelogs/) +[![Telegram group @routeros_scripts](https://img.shields.io/badge/Telegram-%40routeros__scripts-%2326A5E4?logo=telegram&style=flat)](https://t.me/routeros_scripts) +[![donate with PayPal](https://img.shields.io/badge/Like_it%3F-Donate!-orange?logo=githubsponsors&logoColor=orange&style=flat)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=A4ZXBD6YS2W8J) + +[⬅️ Go back to main README](../README.md) + +> ℹ️ **Info**: This script can not be used on its own but requires the base +> installation. See [main README](../README.md) for details. + +Description +----------- + +On *Cloud Hosted Router* (*CHR*) the licensing is perpetual: Buy once, use +forever - but it needs regular renewal. This script checks licensing state +and sends a notification to warn before expiration. + +### Sample notification + +![check-perpetual-license notification](check-perpetual-license.d/notification.avif) + +Requirements and installation +----------------------------- + +Just install the script: + + $ScriptInstallUpdate check-perpetual-license; + +And add a scheduler for automatic update notification: + + /system/scheduler/add interval=1d name=check-perpetual-license on-event="/system/script/run check-perpetual-license;" start-time=startup; + +Configuration +------------- + +No extra configuration is required for this script, but notification +settings are required for +[e-mail](mod/notification-email.md), +[matrix](mod/notification-matrix.md), +[ntfy](mod/notification-ntfy.md) and/or +[telegram](mod/notification-telegram.md). + +Usage and invocation +-------------------- + +Be notified when run from scheduler or run it manually: + + /system/script/run check-perpetual-license; + +Tips & Tricks +------------- + +The script checks for full connectivity before acting, so scheduling at +startup is perfectly valid: + + /system/scheduler/add name=check-perpetual-license@startup on-event="/system/script/run check-perpetual-license;" start-time=startup; + +See also +-------- + +* [Notify on RouterOS update](check-routeros-update.md) + +--- +[⬅️ Go back to main README](../README.md) +[⬆️ Go back to top](#top) diff --git a/doc/check-routeros-update.md b/doc/check-routeros-update.md index 926b4aaa..b6c716cb 100644 --- a/doc/check-routeros-update.md +++ b/doc/check-routeros-update.md @@ -99,6 +99,7 @@ startup is perfectly valid: See also -------- +* [Check perpetual license on CHR](check-perpetual-license.md) * [Automatically upgrade firmware and reboot](firmware-upgrade-reboot.md) * [Manage system update](packages-update.md) diff --git a/global-functions.rsc b/global-functions.rsc index 8ae7bb8e..8ade79bf 100644 --- a/global-functions.rsc +++ b/global-functions.rsc @@ -15,7 +15,7 @@ # Git commit id & info, expected configuration version :global CommitId "unknown"; :global CommitInfo "unknown"; -:global ExpectedConfigVersion 135; +:global ExpectedConfigVersion 136; # global variables not to be changed by user :global GlobalFunctionsReady false; diff --git a/news-and-changes.rsc b/news-and-changes.rsc index 459326f3..55b41650 100644 --- a/news-and-changes.rsc +++ b/news-and-changes.rsc @@ -60,6 +60,7 @@ 133="Updated the default configuration for 'fw-addr-lists', deprecated lists were removed, a collective list was added."; 134="Enhanced 'mod/notification-telegram' and 'telegram-chat' to support topics in groups."; 135="Introduced helper function '\$GetTelegramChatId' for 'mod/notification-telegram' which helps retrieve information."; + 136="Introduced script 'check-perpetual-license' to check for license state on CHR."; }; # Migration steps to be applied on script updates From cdfb086b490cafd685e947433515e43dc5547a62 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Mon, 31 Mar 2025 11:19:23 +0200 Subject: [PATCH 6/7] mod/notification-telegram: fix override quirk IDs for chat and thread can be overridden. Overriding the chat probably makes the thread invalid - so ignore that then. --- mod/notification-telegram.rsc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mod/notification-telegram.rsc b/mod/notification-telegram.rsc index 68e913f6..5ef353b4 100644 --- a/mod/notification-telegram.rsc +++ b/mod/notification-telegram.rsc @@ -151,7 +151,8 @@ :local ChatId [ $EitherOr ($Notification->"chatid") \ [ $EitherOr ($TelegramChatIdOverride->($Notification->"origin")) $TelegramChatId ] ]; :local ThreadId [ $EitherOr ($Notification->"threadid") \ - [ $EitherOr ($TelegramThreadIdOverride->($Notification->"origin")) $TelegramThreadId ] ]; + [ $EitherOr ($TelegramThreadIdOverride->($Notification->"origin")) \ + [ $IfThenElse ([ :len ($TelegramChatIdOverride->($Notification->"origin")) ] = 0) $TelegramThreadId ] ] ]; :local TokenId [ $EitherOr ($TelegramTokenIdOverride->($Notification->"origin")) $TelegramTokenId ]; :if ([ :len $TokenId ] = 0 || [ :len $ChatId ] = 0) do={ From b560ea4b7d3da75b5f54633eea6a5fb7472d5bd6 Mon Sep 17 00:00:00 2001 From: Christian Hesse Date: Mon, 31 Mar 2025 14:30:56 +0200 Subject: [PATCH 7/7] logo: strip some bytes... ... by just re-compressing/re-encoding. --- logo.avif | Bin 2001 -> 1744 bytes logo.png | Bin 4428 -> 4406 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/logo.avif b/logo.avif index 399a2f5ad184fabd7e8127b1e2f1bd994ac7a6b4..956fea8a115623093ec4aef204d69a1a29b53db4 100644 GIT binary patch delta 843 zcmV-R1GN0n56}&eBMpXaWprTx00000001G8FdUInClmsK000C}00IC2009610j>Z5 z0MoIKApw5{Ds5z8bP@mx1{fKy-(gTP#sNfR0frRY%c{{n{hP|N?3Uaqj0L}}@qNfO zUB2aZ8Kr1U{|(j(T$tyh0vOM1q|wTT0X(pK|2Cc9mV0U>am(|lq5{?-y_i?Zr%fu$ zhCDQU=SG}pl+0lejjhL_%w5(HDH(E*okS>|c<+C}P|5$Cf3i?w@OMvy-89`L;#}oM z#2YLk@1}eq#BCuEpbjI5&bIPhY)ZN!fIW7@!JRM9tZSY=8tl(Zn50AhCeIn}bsHqt zVjhC`$G`d1oc$}4gYH%lC=vh)2{;+A-(dj_29Pqp2t;H6VTKq`-8AXdR-Lqe?ll_b zFZZ*n0~`W>ozo$$2fsVSgxpZ#`gr)>n>xw_(#K$B*CM4aHkwEn#JzNubEuV$7=|bA z+ROhT#2ittG2`z=D|OK4zz~ip#0=ynf)@69qgTUtc0+U0uVpYVnm_j8zn0 zJjAjZ9#{~&B=BfFlV5^?`+Bg~S3lvt=dZ1bMkPXj%M`I5p6v2f7J3b@(m+zN6G0CG z{>v?H18V1~@QQG}uh>+c4cY33da3fQ@firsI+uS(o8t{=MtAATH=Lm)TuQAPYya;@v3#M7IYcTEH*jc21m6D)sW-ivCX~$+cSN*r2BpR|Tb22mj|3DNH zT_*pR4q1Qq{z@4w3$5CBwN)%tU^mL(%3q*e9|R8b<56wQ^9`(%3tH$y{UL2-whDf z+PTBdBt(eDqSo^hFgbp(q)#zr2WYWtTbv|w_sz`3(xK`HZ^Wld@#0fDAk(E3KqjJV zz@p4**S2B5Axq>5d?~(DcWcfn@0Gs-wjg(jH~rSuRt;kgdOR8IFNG#knQ0V1TZ3~i z{TlO|tkVnTj)H$$Qb#lavhGSviyk4IJ-g^UwjH~xR7&7SpHW^fe`YmIBX?2Aqr zu8_1h^hG$GGy=y`3LIlI`rBGbwh2kd13ji1;2ei$0-$M4jMe@bt5xL|t${H^4RE-Q zpG%uN7Z!`MfEbi_9^@tGJyxq8x;(wr1VJtc5nJr#OzwsX=}tN(;LpDRJjy5|ddREv zd?KC`i%f&ZEB@53j3wi zl_RoNS*Raoq@rbLZm4~lEaA3srZ@-8A(nYyxW!PEbvL)zS>NC;=@2O@pkAzFEw>>R z#!t2z-sGcGhBZRmWj9_an-~xQN%q|fOQuJ>IA2Jewfl0ctt7)TkciXY5*Z#$ zk8~jc#NV#gn;>c9ZkhO(Xx%U9!h%ElLo)Jw;CDbsZYyeFmmdxIYAGz#837LAKx{w{ z0=K5F+bLRn>s#zg)nWKKMmn}bcWI!ZYkBtH^BNq zrrSDz_1XfK{W_rx^Krge(1;-v#Q@xCU9FIF>7gC9{6qS+MHKcIz<(aA&ha=2IZ8F9 z#8QUWD}S?cFpN9SU5-vgikk;H8#n@Tx5!|`458gpN4b2BUT#>w7= z0CWfOO%ucw+c?ki!hG2fhgu3_1Qnfq#WPK_7b_d|_7AftXk0qc zYNiQDrFZq9_kZ@#=~`%9@4ls-Dt-YZ>XTWJLq|SqWe(SR-hI>BfoA9!n*Y(DvhMy1 diff --git a/logo.png b/logo.png index d97b75dc7d39e2c9e6ff6339d47711e97d6be1f0..7bec10e5242bbc518dc3d730c44425295b283433 100644 GIT binary patch delta 4394 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 delta 4397 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