#!rsc by RouterOS
# RouterOS script: global-functions
# Copyright (c) 2013-2022 Christian Hesse <mail@eworm.de>
# Michael Gisbers <michael@gisbers.de>
# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
#
# global functions
# https://git.eworm.de/cgit/routeros-scripts/about/
:local 0 "global-functions";
# expected configuration version
:global ExpectedConfigVersion 83;
# global variables not to be changed by user
:global GlobalFunctionsReady false;
:global Identity [ /system/identity/get name ];
# global functions
:global CertificateAvailable;
:global CertificateDownload;
:global CertificateNameByCN;
:global CharacterReplace;
:global CleanFilePath;
:global DeviceInfo;
:global DownloadPackage;
:global EitherOr;
:global EscapeForRegEx;
:global FlushEmailQueue;
:global GetMacVendor;
:global GetRandom20CharHex;
:global GetRandomNumber;
:global HexToNum;
:global IfThenElse;
:global IsDefaultRouteReachable;
:global IsDNSResolving;
:global IsFullyConnected;
:global IsTimeSync;
:global LogPrintExit2;
:global MkDir;
:global NotificationFunctions;
:global ParseKeyValueStore;
:global PrettyPrint;
:global QuotedPrintable;
:global RandomDelay;
:global Read;
:global RequiredRouterOS;
:global ScriptFromTerminal;
:global ScriptInstallUpdate;
:global ScriptLock;
:global SendEMail;
:global SendEMail2;
:global SendNotification;
:global SendNotification2;
:global SymbolByUnicodeName;
:global SymbolForNotification;
:global UrlEncode;
:global ValidateSyntax;
:global VersionToNum;
:global WaitDefaultRouteReachable;
:global WaitDNSResolving;
:global WaitForFile;
:global WaitFullyConnected;
:global WaitTimeSync;
# check and download required certificate
:set CertificateAvailable do={
:local CommonName [ :tostr $1 ];
:global CertificateDownload;
:global LogPrintExit2;
:global ParseKeyValueStore;
:if ([ /system/resource/get free-hdd-space ] < 8388608 && \
[ /certificate/settings/get crl-download ] = true && \
[ /certificate/settings/get crl-store ] = "system") do={
$LogPrintExit2 warning $0 ("This system has low free flash space but " . \
"is configured to download certificate CRLs to system!") false;
}
:if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
$LogPrintExit2 info $0 ("Certificate with CommonName \"" . $CommonName . "\" not available.") false;
:if ([ $CertificateDownload $CommonName ] = false) do={
:return false;
}
}
:local CertVal [ /certificate/get [ find where common-name=$CommonName ] ];
:while (($CertVal->"akid") != "" && ($CertVal->"akid") != ($CertVal->"skid")) do={
:if ([ :len [ /certificate/find where skid=($CertVal->"akid") ] ] = 0) do={
$LogPrintExit2 info $0 ("Certificate chain for \"" . $CommonName . \
"\" is incomplete, missing \"" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "\".") false;
:if ([ $CertificateDownload $CommonName ] = false) do={
:return false;
}
}
:set CertVal [ /certificate/get [ find where skid=($CertVal->"akid") ] ];
}
:return true;
}
# download and import certificate
:set CertificateDownload do={
:local CommonName [ :tostr $1 ];
:global ScriptUpdatesBaseUrl;
:global ScriptUpdatesUrlSuffix;
:global CertificateNameByCN;
:global LogPrintExit2;
:global UrlEncode;
:global WaitForFile;
$LogPrintExit2 info $0 ("Downloading and importing certificate with " . \
"CommonName \"" . $CommonName . "\".") false;
:do {
:local LocalFileName ($CommonName . ".pem");
:local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
/tool/fetch check-certificate=yes-without-crl \
($ScriptUpdatesBaseUrl . "certs/" . \
$UrlFileName . $ScriptUpdatesUrlSuffix) \
dst-path=$LocalFileName as-value;
$WaitForFile $LocalFileName;
/certificate/import file-name=$LocalFileName passphrase="" as-value;
/file/remove $LocalFileName;
:foreach Cert in=[ /certificate/find where name~("^" . $LocalFileName . "_[0-9]+\$") ] do={
$CertificateNameByCN [ /certificate/get $Cert common-name ];
}
} on-error={
$LogPrintExit2 warning $0 ("Failed importing certificate with " . \
"CommonName \"" . $CommonName . "\"!") false;
:return false;
}
:return true;
}
# name a certificate by its common-name
:set CertificateNameByCN do={
:local CommonName [ :tostr $1 ];
:global CharacterReplace;
:local Cert [ /certificate/find where common-name=$CommonName ];
/certificate/set $Cert \
name=[ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $CommonName "'" "-" ] " " "-" ] "---" "-" ];
}
# character replace
:set CharacterReplace do={
:local String [ :tostr $1 ];
:local ReplaceFrom [ :tostr $2 ];
:local ReplaceWith [ :tostr $3 ];
:local Return "";
:if ($ReplaceFrom = "") do={
:return $String;
}
:while ([ :typeof [ :find $String $ReplaceFrom ] ] != "nil") do={
:local Pos [ :find $String $ReplaceFrom ];
:set Return ($Return . [ :pick $String 0 $Pos ] . $ReplaceWith);
:set String [ :pick $String ($Pos + [ :len $ReplaceFrom ]) [ :len $String ] ];
}
:return ($Return . $String);
}
# clean file path
:set CleanFilePath do={
:local Path [ :tostr $1 ];
:global CharacterReplace;
:while ($Path ~ "//") do={
:set $Path [ $CharacterReplace $Path "//" "/" ];
}
:if ([ :pick $Path 0 ] = "/") do={
:set Path [ :pick $Path 1 [ :len $Path ] ];
}
:if ([ :pick $Path ([ :len $Path ] - 1) ] = "/") do={
:set Path [ :pick $Path 0 ([ :len $Path ] - 1) ];
}
:return $Path;
}
# get readable device info
:set DeviceInfo do={
:global ExpectedConfigVersion;
:global Identity;
:global IfThenElse;
:local Resource [ /system/resource/get ];
:local RouterBoard;
:do {
:set RouterBoard [[ :parse "/system/routerboard/get" ]];
} on-error={ }
:local License [ /system/license/get ];
:local Update [ /system/package/update/get ];
:return ( \
"Hostname: " . $Identity . \
"\nBoard name: " . $Resource->"board-name" . \
"\nArchitecture: " . $Resource->"architecture-name" . \
[ $IfThenElse ($RouterBoard->"routerboard" = true) \
("\nModel: " . $RouterBoard->"model" . \
[ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
(" " . $RouterBoard->"revision") ] . \
"\nSerial number: " . $RouterBoard->"serial-number") ] . \
[ $IfThenElse ([ :len ($License->"level") ] > 0) \
("\nLicense: " . $License->"level") ] . \
"\nRouterOS:" . \
"\n Channel: " . $Update->"channel" . \
"\n Installed: " . $Update->"installed-version" . \
[ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \
$Update->"installed-version" != $Update->"latest-version") \
("\n Available: " . $Update->"latest-version") ] . \
[ $IfThenElse ($RouterBoard->"routerboard" = true && \
$RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \
("\n Firmware: " . $RouterBoard->"current-firmware") ] . \
"\nRouterOS-Scripts:" . \
"\n Version: " . $ExpectedConfigVersion);
}
# download package from upgrade server
:set DownloadPackage do={
:local PkgName [ :tostr $1 ];
:local PkgVer [ :tostr $2 ];
:local PkgArch [ :tostr $3 ];
:local PkgDir [ :tostr $4 ];
:global CertificateAvailable;
:global CleanFilePath;
:global LogPrintExit2;
:global MkDir;
:global WaitForFile;
:if ([ :len $PkgName ] = 0) do={ :return false; }
:if ([ :len $PkgVer ] = 0) do={ :set PkgVer [ /system/package/update/get installed-version ]; }
:if ([ :len $PkgArch ] = 0) do={ :set PkgArch [ /system/resource/get architecture-name ]; }
:if ($PkgName = "system") do={ :set PkgName "routeros"; }
:local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk");
:if ($PkgArch = "x86_64") do={ :set PkgFile ($PkgName . "-" . $PkgVer . ".npk"); }
:local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];
:if ([ $MkDir $PkgDir ] = false) do={
$LogPrintExit2 warning $0 ("Failed creating directory, not downloading package.") false;
:return false;
}
:if ([ :len [ /file/find where name=$PkgDest type="package" ] ] > 0) do={
$LogPrintExit2 info $0 ("Package file " . $PkgName . " already exists.") false;
:return true;
}
:if ([ $CertificateAvailable "R3" ] = false) do={
$LogPrintExit2 error $0 ("Downloading required certificate failed.") true;
}
:local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile);
$LogPrintExit2 info $0 ("Downloading package file '" . $PkgName . "'...") false;
$LogPrintExit2 debug $0 ("... from url: " . $Url) false;
:local Retry 3;
:while ($Retry > 0) do={
:do {
/tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
$WaitForFile $PkgDest;
:if ([ /file/get [ find where name=$PkgDest ] type ] = "package") do={
:return true;
}
} on-error={
$LogPrintExit2 debug $0 ("Downloading package file failed.") false;
}
/file/remove [ find where name=$PkgDest ];
:set Retry ($Retry - 1);
}
$LogPrintExit2 warning $0 ("Downloading package file '" . $PkgName . "' failed.") false;
:return false;
}
# return either first (if "true") or second
:set EitherOr do={
:global IfThenElse;
:if ([ :typeof $1 ] = "num") do={
:return [ $IfThenElse ($1 != 0) $1 $2 ];
}
:return [ $IfThenElse ([ :len [ :tostr $1 ] ] > 0) $1 $2 ];
}
# escape for regular expression
:set EscapeForRegEx do={
:local Input [ :tostr $1 ];
:if ([ :len $Input ] = 0) do={
:return "";
}
:local Return "";
:local Chars ("^.[]\$()|*+\?{}\\");
:for I from=0 to=([ :len $Input ] - 1) do={
:local Char [ :pick $Input $I ];
:if ([ :find $Chars $Char ]) do={
:set Char ("\\" . $Char);
}
:set Return ($Return . $Char);
}
:return $Return;
}
# flush e-mail queue
:set FlushEmailQueue do={
:global EmailQueue;
:global EitherOr;
:global IsTimeSync;
:global LogPrintExit2;
:local AllDone true;
:local QueueLen [ :len $EmailQueue ];
:if ([ $IsTimeSync ] = false) do={
$LogPrintExit2 debug $0 ("Time is not synced, not flushing.") false;
:return false;
}
:if ([ :len [ /system/scheduler/find where name="FlushEmailQueue" ] ] > 0 && $QueueLen = 0) do={
$LogPrintExit2 warning $0 ("Flushing E-Mail messages from scheduler, but queue is empty.") false;
}
/system/scheduler/set interval=($QueueLen . "m") [ find where name="FlushEmailQueue" ];
:foreach Id,Message in=$EmailQueue do={
:if ([ :typeof $Message ] = "array" ) do={
:local Attach [ $EitherOr ($Message->"attach") "" ];
:while ([ /tool/e-mail/get last-status ] = "in-progress") do={ :delay 1s; }
/tool/e-mail/send to=($Message->"to") cc=($Message->"cc") subject=($Message->"subject") \
body=($Message->"body") file=$Attach;
:local Wait true;
:do {
:delay 1s;
:local Status [ /tool/e-mail/get last-status ];
:if ($Status = "succeeded") do={
:set ($EmailQueue->$Id);
:set Wait false;
:if (($Message->"remove-attach") = true) do={
:foreach File in=[ :toarray $Attach ] do={
/file/remove $File;
}
}
}
:if ($Status = "failed") do={
:set AllDone false;
:set Wait false;
}
} while=($Wait = true);
}
}
:if ($AllDone = true && $QueueLen = [ :len $EmailQueue ]) do={
/system/scheduler/remove [ find where name="FlushEmailQueue" ];
:set EmailQueue;
} else={
/system/scheduler/set interval=1m [ find where name="FlushEmailQueue" ];
}
}
# get MAC vendor
:set GetMacVendor do={
:local Mac [ :tostr $1 ];
:global CertificateAvailable;
:global LogPrintExit2;
:do {
:if ([ $CertificateAvailable "Cloudflare Inc ECC CA-3" ] = false) do={
$LogPrintExit2 warning $0 ("Downloading required certificate failed.") true;
}
:local Vendor ([ /tool/fetch check-certificate=yes-without-crl \
("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data");
:return $Vendor;
} on-error={
:do {
/tool/fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \
output=none as-value;
$LogPrintExit2 debug $0 ("The mac vendor is not known in database.") false;
} on-error={
$LogPrintExit2 warning $0 ("Failed getting mac vendor.") false;
}
:return "unknown vendor";
}
}
# generate random 20 chars hex (0-9 and a-f)
:set GetRandom20CharHex do={
:return [ :rndstr length=20 from="0123456789abcdef" ];
}
# generate random number
:set GetRandomNumber do={
:local Max 4294967295;
:if ([ :typeof $1 ] != "nothing" ) do={
:set Max ([ :tonum $1 ] + 1);
}
:global GetRandom20CharHex;
:global HexToNum;
:return ([ $HexToNum [ :pick [ $GetRandom20CharHex ] 0 15 ] ] % $Max);
}
# convert from hex (string) to num
:set HexToNum do={
:local Input [ :tostr $1 ];
:local Hex "0123456789abcdef0123456789ABCDEF";
:local Multi 1;
:local Return 0;
:for I from=([ :len $Input ] - 1) to=0 do={
:set Return ($Return + (([ :find $Hex [ :pick $Input $I ] ] % 16) * $Multi));
:set Multi ($Multi * 16);
}
:return $Return;
}
# mimic conditional/ternary operator (condition ? consequent : alternative)
:set IfThenElse do={
:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={
:return $2;
}
:return $3;
}
# check if default route is reachable
:set IsDefaultRouteReachable do={
:if ([ :len [ /ip/route/find where dst-address=0.0.0.0/0 active routing-table=main ] ] > 0) do={
:return true;
}
:return false;
}
# check if DNS is resolving
:set IsDNSResolving do={
:global CharacterReplace;
:do {
:resolve "low-ttl.eworm.de";
} on-error={
:return false;
}
:return true;
}
# check if system is is fully connected (default route reachable, DNS resolving, time sync)
:set IsFullyConnected do={
:global IsDefaultRouteReachable;
:global IsDNSResolving;
:global IsTimeSync;
:if ([ $IsDefaultRouteReachable ] = false) do={
:return false;
}
:if ([ $IsDNSResolving ] = false) do={
:return false;
}
:if ([ $IsTimeSync ] = false) do={
:return false;
}
:return true;
}
# check if system time is sync
:set IsTimeSync do={
:global IsTimeSyncCached;
:global LogPrintExit2;
:if ($IsTimeSyncCached = true) do={
:return true;
}
:if ([ /system/ntp/client/get enabled ] = true) do={
:if ([ /system/ntp/client/get status ] = "synchronized") do={
:set IsTimeSyncCached true;
:return true;
}
:return false;
}
:if ([ /ip/cloud/get update-time ] = true) do={
:if ([ :typeof [ /ip/cloud/get public-address ] ] = "ip") do={
:set IsTimeSyncCached true;
:return true;
}
:return false;
}
$LogPrintExit2 debug $0 ("No time source configured! Returning gracefully...") false;
:return true;
}
# log and print with same text, optionally exit
:set LogPrintExit2 do={
:local Severity [ :tostr $1 ];
:local Name [ :tostr $2 ];
:local Message [ :tostr $3 ];
:local Exit [ :tostr $4 ];
:global PrintDebug;
:global PrintDebugOverride;
:global EitherOr;
:local Debug [ $EitherOr ($PrintDebugOverride->$Name) $PrintDebug ];
:local PrintSeverity do={
:global TerminalColorOutput;
:if ($TerminalColorOutput != true) do={
:return $1;
}
:local Color { debug=96; info=97; warning=93; error=91 };
:return ("\1B[" . $Color->$1 . "m" . $1 . "\1B[0m");
}
:local Log ([ $EitherOr $Name "<unknown>" ] . ": " . $Message);
:if ($Severity ~ ("^(debug|error|info)\$")) do={
:if ($Severity = "debug") do={ :log debug $Log; }
:if ($Severity = "error") do={ :log error $Log; }
:if ($Severity = "info" ) do={ :log info $Log; }
} else={
:log warning $Log;
:set Severity "warning";
}
:if ($Severity != "debug" || $Debug = true) do={
:if ($Exit = "true") do={
:error ([ $PrintSeverity $Severity ] . ": " . $Message);
} else={
:put ([ $PrintSeverity $Severity ] . ": " . $Message);
}
}
}
# create directory
:set MkDir do={
:local Dir [ :tostr $1 ];
:global CleanFilePath;
:global GetRandom20CharHex;
:global LogPrintExit2;
:global WaitForFile;
:set Dir [ $CleanFilePath $Dir ];
:if ($Dir = "") do={
:return true;
}
:if ([ :len [ /file/find where name=$Dir type="directory" ] ] = 1) do={
:return true;
}
:local Return true;
:local Name ($Dir . "-" . [ $GetRandom20CharHex ]);
:do {
/ip/smb/share/add disabled=yes directory=$Dir name=$Name;
$WaitForFile $Dir;
} on-error={
$LogPrintExit2 warning $0 ("Making directory '" . $Dir . "' failed!") false;
:set Return false;
}
/ip/smb/share/remove [ find where name=$Name ];
:return $Return;
}
# prepare NotificationFunctions array
:if ([ :typeof $NotificationFunctions ] != "array") do={
:set NotificationFunctions ({});
}
# send notification via e-mail - expects one array argument
:set ($NotificationFunctions->"email") do={
:local Notification $1;
:global Identity;
:global EmailGeneralTo;
:global EmailGeneralToOverride;
:global EmailGeneralCc;
:global EmailGeneralCcOverride;
:global EmailQueue;
:global EitherOr;
:global IfThenElse;
:global QuotedPrintable;
:local To [ $EitherOr ($EmailGeneralToOverride->($Notification->"origin")) $EmailGeneralTo ];
:local Cc [ $EitherOr ($EmailGeneralCcOverride->($Notification->"origin")) $EmailGeneralCc ];
:local EMailSettings [ /tool/e-mail/get ];
:if ([ :len $To ] = 0 || ($EMailSettings->"address") = "0.0.0.0" || ($EMailSettings->"from") = "<>") do={
:return false;
}
:if ([ :typeof $EmailQueue ] = "nothing") do={
:set EmailQueue ({});
}
:local Signature [ /system/note/get note ];
:set ($EmailQueue->[ :len $EmailQueue ]) {
to=$To; cc=$Cc;
subject=[ $QuotedPrintable ("[" . $Identity . "] " . ($Notification->"subject")) ];
body=(($Notification->"message") . \
[ $IfThenElse ([ :len ($Notification->"link") ] > 0) ("\n\n" . ($Notification->"link")) "" ] . \
[ $IfThenElse ([ :len $Signature ] > 0) ("\n-- \n" . $Signature) "" ]); \
attach=($Notification->"attach"); remove-attach=($Notification->"remove-attach") };
:if ([ :len [ /system/scheduler/find where name="FlushEmailQueue" ] ] = 0) do={
/system/scheduler/add name=FlushEmailQueue interval=1s start-time=startup \
on-event=(":global FlushEmailQueue; \$FlushEmailQueue;");
}
}
# parse key value store
:set ParseKeyValueStore do={
:local Source $1;
:if ([ :typeof $Source ] != "array") do={
:set Source [ :tostr $1 ];
}
:local Result ({});
:foreach KeyValue in=[ :toarray $Source ] do={
:if ([ :find $KeyValue "=" ]) do={
:set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \
[ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
} else={
:set ($Result->$KeyValue) true;
}
}
:return $Result;
}
# print lines with trailing carriage return
:set PrettyPrint do={
:local Input [ :tostr $1 ];
:global CharacterReplace;
:put [ $CharacterReplace $Input ("\n") ("\n\r") ];
}
# convert string to quoted-printable
:global QuotedPrintable do={
:local Input [ :tostr $1 ];
:if ([ :len $Input ] = 0) do={
:return $Input;
}
:local Return "";
:local Chars ("\80\81\82\83\84\85\86\87\88\89\8A\8B\8C\8D\8E\8F\90\91\92\93\94\95\96\97" . \
"\98\99\9A\9B\9C\9D\9E\9F\A0\A1\A2\A3\A4\A5\A6\A7\A8\A9\AA\AB\AC\AD\AE\AF\B0\B1\B2\B3" . \
"\B4\B5\B6\B7\B8\B9\BA\BB\BC\BD\BE\BF\C0\C1\C2\C3\C4\C5\C6\C7\C8\C9\CA\CB\CC\CD\CE\CF" . \
"\D0\D1\D2\D3\D4\D5\D6\D7\D8\D9\DA\DB\DC\DD\DE\DF\E0\E1\E2\E3\E4\E5\E6\E7\E8\E9\EA\EB" . \
"\EC\ED\EE\EF\F0\F1\F2\F3\F4\F5\F6\F7\F8\F9\FA\FB\FC\FD\FE\FF");
:local Hex { "0"; "1"; "2"; "3"; "4"; "5"; "6"; "7"; "8"; "9"; "A"; "B"; "C"; "D"; "E"; "F" };
:for I from=0 to=([ :len $Input ] - 1) do={
:local Char [ :pick $Input $I ];
:local Replace [ :find $Chars $Char ];
:if ($Char = "=") do={
:set Char "=3D";
}
:if ([ :typeof $Replace ] = "num") do={
:set Char ("=" . ($Hex->($Replace / 16 + 8)) . ($Hex->($Replace % 16)));
}
:set Return ($Return . $Char);
}
:if ($Input = $Return) do={
:return $Input;
}
:return ("=\?utf-8\?Q\?" . $Return . "\?=");
}
# delay a random amount of seconds
:set RandomDelay do={
:global EitherOr;
:global GetRandomNumber;
:delay ([ $GetRandomNumber $1 ] . [ $EitherOr $2 "s" ]);
}
# read input from user
:set Read do={
:return;
}
# check for required RouterOS version
:set RequiredRouterOS do={
:local Caller [ :tostr $1 ];
:local Required [ :tostr $2 ];
:local Warn [ :tostr $3 ];
:global IfThenElse;
:global LogPrintExit2;
:global VersionToNum;
:if ([ $VersionToNum $Required ] > [ $VersionToNum [ /system/package/update/get installed-version ] ]) do={
:if ($Warn = "true") do={
$LogPrintExit2 warning $0 ("This " . [ $IfThenElse ([ :pick $Caller 0 ] = ("\$")) "function" "script" ] . \
" '" . $Caller . "' (at least specific functionality) requires RouterOS " . $Required . ". Please update!") false;
}
:return false;
}
:return true;
}
# check if script is run from terminal
:set ScriptFromTerminal do={
:local Script [ :tostr $1 ];
:global LogPrintExit2;
:foreach Job in=[ /system/script/job/find where script=$Script ] do={
:set Job [ /system/script/job/get $Job ];
:while ([ :typeof ($Job->"parent") ] = "id") do={
:set Job [ /system/script/job/get [ find where .id=($Job->"parent") ] ];
}
:if (($Job->"type") = "login") do={
$LogPrintExit2 debug $0 ("Script " . $Script . " started from terminal.") false;
:return true;
}
}
$LogPrintExit2 debug $0 ("Script " . $Script . " NOT started from terminal.") false;
:return false;
}
# install new scripts, update existing scripts
:set ScriptInstallUpdate do={
:local Scripts [ :toarray $1 ];
:local NewComment [ :tostr $2 ];
:global ExpectedConfigVersion;
:global Identity;
:global IDonate;
:global NoNewsAndChangesNotification;
:global NotificationsWithSymbols;
:global ScriptUpdatesBaseUrl;
:global ScriptUpdatesFetch;
:global ScriptUpdatesUrlSuffix;
:global SentConfigChangesNotification;
:global CertificateAvailable;
:global IfThenElse;
:global LogPrintExit2;
:global ParseKeyValueStore;
:global SendNotification2;
:global SymbolForNotification;
:global ValidateSyntax;
:if ([ $CertificateAvailable "R3" ] = false) do={
$LogPrintExit2 warning $0 ("Downloading certificate failed, trying without.") false;
}
:foreach Script in=$Scripts do={
:if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
$LogPrintExit2 info $0 ("Adding new script: " . $Script) false;
/system/script/add name=$Script owner=$Script source="#!rsc by RouterOS\n" comment=$NewComment;
}
}
:local ExpectedConfigVersionBefore $ExpectedConfigVersion;
:local ReloadGlobalFunctions false;
:local ReloadGlobalConfig false;
:foreach Script in=[ /system/script/find where source~"^#!rsc by RouterOS\n" ] do={
:local ScriptVal [ /system/script/get $Script ];
:local ScriptFile [ /file/find where name=("script-updates/" . $ScriptVal->"name") ];
:local SourceNew;
:if ([ :len $ScriptFile ] > 0) do={
:set SourceNew [ /file/get $ScriptFile content ];
/file/remove $ScriptFile;
}
:foreach Scheduler in=[ /system/scheduler/find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={
:local SchedulerVal [ /system/scheduler/get $Scheduler ];
:if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={
$LogPrintExit2 warning $0 ("Policies differ for script '" . $ScriptVal->"name" . \
"' and its scheduler '" . $SchedulerVal->"name" . "'!") false;
}
}
:if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
:local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ];
:if (!($Comment->"ignore" = true)) do={
:do {
:local BaseUrl $ScriptUpdatesBaseUrl;
:local UrlSuffix $ScriptUpdatesUrlSuffix;
:if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); }
:if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); }
:local Url ($BaseUrl . $ScriptVal->"name" . $UrlSuffix);
$LogPrintExit2 debug $0 ("Fetching script '" . $ScriptVal->"name" . "' from url: " . $Url) false;
:local Result [ /tool/fetch check-certificate=yes-without-crl $Url output=user as-value ];
:if ($Result->"status" = "finished") do={
:set SourceNew ($Result->"data");
}
} on-error={
:if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={
$LogPrintExit2 warning $0 ("Failed fetching script '" . $ScriptVal->"name" . \
"', removing dummy. Typo on installation?") false;
/system/script/remove $Script;
} else={
$LogPrintExit2 warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!") false;
}
}
}
}
:if ([ :len $SourceNew ] > 0) do={
:if ($SourceNew != $ScriptVal->"source") do={
:if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
:if ([ $ValidateSyntax $SourceNew ] = true) do={
:local DontRequirePermissions \
($SourceNew~"\n# requires: dont-require-permissions=yes\n");
$LogPrintExit2 info $0 ("Updating script: " . $ScriptVal->"name") false;
/system/script/set owner=($ScriptVal->"name") source=$SourceNew \
dont-require-permissions=$DontRequirePermissions $Script;
:if ($ScriptVal->"name" = "global-config") do={
:set ReloadGlobalConfig true;
}
:if ($ScriptVal->"name" = "global-functions" || $ScriptVal->"name" ~ ("^mod/.")) do={
:set ReloadGlobalFunctions true;
}
} else={
$LogPrintExit2 warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \
"' failed! Ignoring!") false;
}
} else={
$LogPrintExit2 warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
"' is not valid (missing shebang). Ignoring!") false;
}
} else={
$LogPrintExit2 debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.") false;
}
} else={
$LogPrintExit2 debug $0 ("No update for script '" . $ScriptVal->"name" . "'.") false;
}
}
:if ($ReloadGlobalFunctions = true) do={
$LogPrintExit2 info $0 ("Reloading global functions.") false;
:do {
/system/script/run global-functions;
} on-error={
$LogPrintExit2 error $0 ("Reloading global functions failed!") false;
}
}
:if ($ReloadGlobalConfig = true) do={
$LogPrintExit2 info $0 ("Reloading global configuration.") false;
:do {
/system/script/run global-config;
} on-error={
$LogPrintExit2 error $0 ("Reloading global configuration failed!" . \
" Syntax error or missing overlay\?") false;
}
}
:if ($ExpectedConfigVersionBefore != $ExpectedConfigVersion) do={
:global GlobalConfigChanges;
:global GlobalConfigMigration;
:local ChangeLogCode;
:do {
:local Url ($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix);
$LogPrintExit2 debug $0 ("Fetching news, changes and migration: " . $Url) false;
:local Result [ /tool/fetch check-certificate=yes-without-crl $Url output=user as-value ];
:if ($Result->"status" = "finished") do={
:set ChangeLogCode ($Result->"data");
}
} on-error={
$LogPrintExit2 warning $0 ("Failed fetching news, changes and migration!") false;
}
:if ([ :len $ChangeLogCode ] > 0) do={
:if ([ $ValidateSyntax $ChangeLogCode ] = true) do={
:do {
[ :parse $ChangeLogCode ];
} on-error={
$LogPrintExit2 warning $0 ("The changelog failed to run!") false;
}
} else={
$LogPrintExit2 warning $0 ("The changelog failed syntax validation!") false;
}
}
:if ([ :len $GlobalConfigMigration ] > 0) do={
:for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={
:local Migration ($GlobalConfigMigration->[ :tostr $I ]);
:if ([ :typeof $Migration ] = "str") do={
:if ([ $ValidateSyntax $Migration ] = true) do={
$LogPrintExit2 info $0 ("Applying migration for change " . $I . ": " . $Migration) false;
:do {
[ :parse $Migration ];
} on-error={
$LogPrintExit2 warning $0 ("Migration code for change " . $I . " failed to run!") false;
}
} else={
$LogPrintExit2 warning $0 ("Migration code for change " . $I . " failed syntax validation!") false;
}
}
}
}
:if ($SentConfigChangesNotification != $ExpectedConfigVersion && \
$NoNewsAndChangesNotification != true) do={
:local NotificationMessage ("The configuration version on " . $Identity . " increased " . \
"to " . $ExpectedConfigVersion . ", current configuration may need modification. " . \
"Please review and update global-config-overlay, then re-run global-config.");
$LogPrintExit2 info $0 ($NotificationMessage) false;
:if ([ :len $GlobalConfigChanges ] > 0) do={
:set NotificationMessage ($NotificationMessage . "\n\nChanges:");
:for I from=($ExpectedConfigVersionBefore + 1) to=$ExpectedConfigVersion do={
:local Change ($GlobalConfigChanges->[ :tostr $I ]);
:set NotificationMessage ($NotificationMessage . "\n " . \
[ $IfThenElse ($NotificationsWithSymbols = true) ("\E2\97\8F") "*" ] . " " . $Change);
$LogPrintExit2 info $0 ("Change " . $I . ": " . $Change) false;
}
} else={
:set NotificationMessage ($NotificationMessage . "\n\nNews and changes are not available.");
}
:local Link;
:if ($IDonate != true) do={
:set NotificationMessage ($NotificationMessage . \
"\n\n==== donation hint ====\n" . \
"This project is developed in private spare time and usage is " . \
"free of charge for you. If you like the scripts and think this is " . \
"of value for you or your business please consider a donation.");
:set Link "https://git.eworm.de/cgit/routeros-scripts/about/#donate";
}
$SendNotification2 ({ origin=$0; \
subject=([ $SymbolForNotification "pushpin" ] . "News and configuration changes"); \
message=$NotificationMessage; link=$Link });
:set SentConfigChangesNotification $ExpectedConfigVersion;
}
:set GlobalConfigChanges;
:set GlobalConfigMigration;
}
}
# lock script against multiple invocation
:set ScriptLock do={
:local Script [ :tostr $1 ];
:local DoReturn $2;
:local WaitMax ([ :tonum $3 ] * 10);
:global GetRandom20CharHex;
:global IfThenElse;
:global LogPrintExit2;
:global ScriptLockOrder;
:if ([ :typeof $ScriptLockOrder ] = "nothing") do={
:set ScriptLockOrder ({});
}
:if ([ :typeof ($ScriptLockOrder->$Script) ] = "nothing") do={
:set ($ScriptLockOrder->$Script) ({});
}
:local JobCount do={
:local Script [ :tostr $1 ];
:return [ :len [ /system/script/job/find where script=$Script ] ];
}
:local TicketCount do={
:local Script [ :tostr $1 ];
:global ScriptLockOrder;
:local Count 0;
:foreach Ticket in=($ScriptLockOrder->$Script) do={
:if ([ :typeof $Ticket ] != "nothing") do={
:set Count ($Count + 1);
}
}
:return $Count;
}
:local IsFirstTicket do={
:local Script [ :tostr $1 ];
:local Check [ :tostr $2 ];
:global ScriptLockOrder;
:foreach Ticket in=($ScriptLockOrder->$Script) do={
:if ($Ticket = $Check) do={ :return true; }
:if ([ :typeof $Ticket ] != "nothing" && $Ticket != $Check) do={ :return false; }
}
:return false;
}
:local AddTicket do={
:local Script [ :tostr $1 ];
:local Add [ :tostr $2 ];
:global ScriptLockOrder;
:while (true) do={
:local Pos [ :len ($ScriptLockOrder->$Script) ];
:set ($ScriptLockOrder->$Script->$Pos) $Add;
:delay 10ms;
:if (($ScriptLockOrder->$Script->$Pos) = $Add) do={ :return true; }
}
}
:local RemoveTicket do={
:local Script [ :tostr $1 ];
:local Remove [ :tostr $2 ];
:global ScriptLockOrder;
:foreach Id,Ticket in=($ScriptLockOrder->$Script) do={
:while (($ScriptLockOrder->$Script->$Id) = $Remove) do={
:set ($ScriptLockOrder->$Script->$Id);
:delay 10ms;
}
}
}
:local CleanupTickets do={
:local Script [ :tostr $1 ];
:global ScriptLockOrder;
:foreach Ticket in=($ScriptLockOrder->$Script) do={
:if ([ :typeof $Ticket ] != "nothing") do={
:return false;
}
}
:set ($ScriptLockOrder->$Script) ({});
}
:if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
$LogPrintExit2 error $0 ("A script named '" . $Script . "' does not exist!") true;
}
:if ([ $JobCount $Script ] = 0) do={
$LogPrintExit2 error $0 ("No script '" . $Script . "' is running!") true;
}
:if ([ $TicketCount $Script ] >= [ $JobCount $Script ]) do={
$LogPrintExit2 error $0 ("More tickets than running scripts '" . $Script . "', resetting!") false;
:set ($ScriptLockOrder->$Script) ({});
/system/script/job/remove [ find where script=$Script ];
}
:local MyTicket [ $GetRandom20CharHex ];
$AddTicket $Script $MyTicket;
:local WaitCount 0;
:while ($WaitMax > $WaitCount && ([ $IsFirstTicket $Script $MyTicket ] = false || [ $TicketCount $Script ] < [ $JobCount $Script ])) do={
:set WaitCount ($WaitCount + 1);
:delay 100ms;
}
:if ([ $IsFirstTicket $Script $MyTicket ] = true && [ $TicketCount $Script ] = [ $JobCount $Script ]) do={
$RemoveTicket $Script $MyTicket;
$CleanupTickets $Script;
:return false;
}
$RemoveTicket $Script $MyTicket;
$LogPrintExit2 info $0 ("Script '" . $Script . "' started more than once" . [ $IfThenElse ($WaitCount > 0) \
" and timed out waiting for lock" "" ] . "... Aborting.") [ $IfThenElse ($DoReturn = true) false true ];
:return true;
}
# send notification via e-mail - expects at least two string arguments
:set SendEMail do={
:global SendEMail2;
$SendEMail2 ({ subject=$1; message=$2; link=$3 });
}
# send notification via e-mail - expects one array argument
:set SendEMail2 do={
:local Notification $1;
:global NotificationFunctions;
($NotificationFunctions->"email") ("\$NotificationFunctions->\"email\"") $Notification;
}
# send notification via NotificationFunctions - expects at least two string arguments
:set SendNotification do={
:global SendNotification2;
$SendNotification2 ({ subject=$1; message=$2; link=$3; silent=$4 });
}
# send notification via NotificationFunctions - expects one array argument
:set SendNotification2 do={
:local Notification $1;
:global NotificationFunctions;
:foreach FunctionName,Discard in=$NotificationFunctions do={
($NotificationFunctions->$FunctionName) \
("\$NotificationFunctions->\"" . $FunctionName . "\"") \
$Notification;
}
}
# return UTF-8 symbol for unicode name
:set SymbolByUnicodeName do={
:local Symbols {
"alarm-clock"="\E2\8F\B0";
"calendar"="\F0\9F\93\85";
"chart-decreasing"="\F0\9F\93\89";
"chart-increasing"="\F0\9F\93\88";
"cloud"="\E2\98\81";
"cross-mark"="\E2\9D\8C";
"fire"="\F0\9F\94\A5";
"floppy-disk"="\F0\9F\92\BE";
"high-voltage-sign"="\E2\9A\A1";
"incoming-envelope"="\F0\9F\93\A8";
"link"="\F0\9F\94\97";
"lock-with-ink-pen"="\F0\9F\94\8F";
"memo"="\F0\9F\93\9D";
"mobile-phone"="\F0\9F\93\B1";
"pushpin"="\F0\9F\93\8C";
"scissors"="\E2\9C\82";
"sparkles"="\E2\9C\A8";
"up-arrow"="\E2\AC\86";
"warning-sign"="\E2\9A\A0";
"white-heavy-check-mark"="\E2\9C\85"
}
:return ($Symbols->$1);
}
# return symbol for notification
:set SymbolForNotification do={
:global NotificationsWithSymbols;
:global SymbolByUnicodeName;
:if ($NotificationsWithSymbols != true) do={
:return "";
}
:local Return "";
:foreach Symbol in=[ :toarray $1 ] do={
:set Return ($Return . [ $SymbolByUnicodeName $Symbol ]);
}
:return ($Return . " ");
}
# url encoding
:set UrlEncode do={
:local Input [ :tostr $1 ];
:if ([ :len $Input ] = 0) do={
:return "";
}
:local Return "";
:local Chars ("\n\r !\"#\$%&'()*+,:;<=>\?@[\\]^`{|}~");
:local Subs { "%0A"; "%0D"; "%20"; "%21"; "%22"; "%23"; "%24"; "%25"; "%26"; "%27";
"%28"; "%29"; "%2A"; "%2B"; "%2C"; "%3A"; "%3B"; "%3C"; "%3D"; "%3E"; "%3F";
"%40"; "%5B"; "%5C"; "%5D"; "%5E"; "%60"; "%7B"; "%7C"; "%7D"; "%7E" };
:for I from=0 to=([ :len $Input ] - 1) do={
:local Char [ :pick $Input $I ];
:local Replace [ :find $Chars $Char ];
:if ([ :typeof $Replace ] = "num") do={
:set Char ($Subs->$Replace);
}
:set Return ($Return . $Char);
}
:return $Return;
}
# basic syntax validation
:set ValidateSyntax do={
:local Code [ :tostr $1 ];
:do {
[ :parse (":local Validate do={\n" . $Code . "\n}") ];
} on-error={
:return false;
}
:return true;
}
# convert version string to numeric value
:set VersionToNum do={
:local Input [ :tostr $1 ];
:local Multi 0x1000000;
:local Return 0;
:global CharacterReplace;
:set Input [ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $Input \
"." "," ] "beta" ",beta," ] "rc" ",rc," ];
:foreach Value in=([ :toarray $Input ], 0) do={
:local Num [ :tonum $Value ];
:if ($Multi = 0x100) do={
:if ([ :typeof $Num ] = "num") do={
:set Return ($Return + 0xff00);
:set Multi ($Multi / 0x100);
} else={
:if ($Value = "beta") do={ :set Return ($Return + 0x3f00); }
:if ($Value = "rc") do={ :set Return ($Return + 0x7f00); }
}
}
:if ([ :typeof $Num ] = "num") do={ :set Return ($Return + ($Value * $Multi)); }
:set Multi ($Multi / 0x100);
}
:return $Return;
}
# wait for default route to be reachable
:set WaitDefaultRouteReachable do={
:global IsDefaultRouteReachable;
:while ([ $IsDefaultRouteReachable ] = false) do={
:delay 1s;
}
}
# wait for DNS to resolve
:set WaitDNSResolving do={
:global IsDNSResolving;
:while ([ $IsDNSResolving ] = false) do={
:delay 1s;
}
}
# wait for file to be available
:set WaitForFile do={
:local FileName [ :tostr $1 ];
:global CleanFilePath;
:set FileName [ $CleanFilePath $FileName ];
:local I 0;
:while ([ :len [ /file/find where name=$FileName ] ] = 0) do={
:if ($I > 20) do={
:return false;
}
:delay 100ms;
:set I ($I + 1);
}
:return true;
}
# wait to be fully connected (default route is reachable, time is sync, DNS resolves)
:set WaitFullyConnected do={
:global WaitDefaultRouteReachable;
:global WaitDNSResolving;
:global WaitTimeSync;
$WaitDefaultRouteReachable;
$WaitTimeSync;
$WaitDNSResolving;
}
# wait for time to become synced
:set WaitTimeSync do={
:global IsTimeSync;
:while ([ $IsTimeSync ] = false) do={
:delay 1s;
}
}
# load modules
:foreach Script in=[ /system/script/find where name ~ "^mod/." ] do={
:local ScriptVal [ /system/script/get $Script ];
:if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={
:do {
/system/script/run $Script;
} on-error={
$LogPrintExit2 error $0 ("Module '" . $ScriptVal->"name" . "' failed to run.") false;
}
} else={
$LogPrintExit2 error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.") false;
}
}
# check for required RouterOS version
$RequiredRouterOS $0 "7.1" true;
# signal we are ready
:set GlobalFunctionsReady true;