aboutsummaryrefslogtreecommitdiffstats
path: root/global-functions.rsc
diff options
context:
space:
mode:
Diffstat (limited to 'global-functions.rsc')
-rw-r--r--global-functions.rsc953
1 files changed, 690 insertions, 263 deletions
diff --git a/global-functions.rsc b/global-functions.rsc
index 8c96c72..71aa7de 100644
--- a/global-functions.rsc
+++ b/global-functions.rsc
@@ -1,35 +1,43 @@
#!rsc by RouterOS
# RouterOS script: global-functions
-# Copyright (c) 2013-2023 Christian Hesse <mail@eworm.de>
+# Copyright (c) 2013-2025 Christian Hesse <mail@eworm.de>
# Michael Gisbers <michael@gisbers.de>
-# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
+# https://rsc.eworm.de/COPYING.md
#
-# requires RouterOS, version=7.9beta4
+# requires RouterOS, version=7.15
+# requires device-mode, fetch, scheduler
#
# global functions
-# https://git.eworm.de/cgit/routeros-scripts/about/
+# https://rsc.eworm.de/
-:local 0 "global-functions";
+:local ScriptName [ :jobname ];
-# expected configuration version
-:global ExpectedConfigVersion 105;
+# Git commit id & info, expected configuration version
+:global CommitId "unknown";
+:global CommitInfo "unknown";
+:global ExpectedConfigVersion 137;
# global variables not to be changed by user
:global GlobalFunctionsReady false;
-:global FetchUserAgent ("User-Agent: Mikrotik/" . [ /system/resource/get version ] . " Fetch");
:global Identity [ /system/identity/get name ];
# global functions
+:global AlignRight;
:global CertificateAvailable;
:global CertificateDownload;
:global CertificateNameByCN;
+:global CharacterMultiply;
:global CharacterReplace;
:global CleanFilePath;
+:global CleanName;
:global DeviceInfo;
:global Dos2Unix;
:global DownloadPackage;
:global EitherOr;
:global EscapeForRegEx;
+:global ExitError;
+:global FetchHuge;
+:global FetchUserAgentStr;
:global FormatLine;
:global FormatMultiLines;
:global GetMacVendor;
@@ -38,21 +46,28 @@
:global GetRandomNumber;
:global Grep;
:global HexToNum;
+:global HumanReadableNum;
:global IfThenElse;
:global IsDefaultRouteReachable;
:global IsDNSResolving;
:global IsFullyConnected;
:global IsMacLocallyAdministered;
:global IsTimeSync;
-:global LogPrintExit2;
+:global LogPrint;
+:global LogPrintOnce;
+:global LogPrintVerbose;
+:global MAX;
+:global MIN;
:global MkDir;
:global NotificationFunctions;
:global ParseDate;
:global ParseKeyValueStore;
:global PrettyPrint;
+:global ProtocolStrip;
:global RandomDelay;
-:global Read;
:global RequiredRouterOS;
+:global RmDir;
+:global RmFile;
:global ScriptFromTerminal;
:global ScriptInstallUpdate;
:global ScriptLock;
@@ -70,23 +85,42 @@
:global WaitFullyConnected;
:global WaitTimeSync;
+# align string to the right
+:set AlignRight do={
+ :local Input [ :tostr $1 ];
+ :local Len [ :tonum $2 ];
+
+ :global CharacterMultiply;
+ :global EitherOr;
+
+ :set Len [ $EitherOr $Len 8 ];
+ :local Spaces [ $CharacterMultiply " " $Len ];
+
+ :return ([ :pick $Spaces 0 ($Len - [ :len $Input ]) ] . $Input);
+}
+
# check and download required certificate
:set CertificateAvailable do={
:local CommonName [ :tostr $1 ];
:global CertificateDownload;
- :global LogPrintExit2;
+ :global LogPrint;
: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;
+ $LogPrint warning $0 ("This system has low free flash space but " . \
+ "is configured to download certificate CRLs to system!");
+ }
+
+ :if ([ :len $CommonName ] = 0) do={
+ $LogPrint warning $0 ("No CommonName given!");
+ :return false;
}
:if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
- $LogPrintExit2 info $0 ("Certificate with CommonName \"" . $CommonName . "\" not available.") false;
+ $LogPrint info $0 ("Certificate with CommonName '" . $CommonName . "' not available.");
:if ([ $CertificateDownload $CommonName ] = false) do={
:return false;
}
@@ -95,8 +129,8 @@
: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;
+ $LogPrint info $0 ("Certificate chain for '" . $CommonName . \
+ "' is incomplete, missing '" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "'.");
:if ([ $CertificateDownload $CommonName ] = false) do={
:return false;
}
@@ -110,48 +144,87 @@
:set CertificateDownload do={
:local CommonName [ :tostr $1 ];
- :global FetchUserAgent;
:global ScriptUpdatesBaseUrl;
:global ScriptUpdatesUrlSuffix;
+ :global CertificateAvailable;
:global CertificateNameByCN;
- :global LogPrintExit2;
- :global UrlEncode;
+ :global CleanName;
+ :global FetchUserAgentStr;
+ :global LogPrint;
+ :global RmFile;
:global WaitForFile;
- $LogPrintExit2 info $0 ("Downloading and importing certificate with " . \
- "CommonName \"" . $CommonName . "\".") false;
+ $LogPrint info $0 ("Downloading and importing certificate with " . \
+ "CommonName '" . $CommonName . "'.");
+ :local FileName ([ $CleanName $CommonName ] . ".pem");
:do {
- :local LocalFileName ($CommonName . ".pem");
- :local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
- /tool/fetch check-certificate=yes-without-crl http-header-field=$FetchUserAgent \
- ($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 ];
- }
+ /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \
+ ($ScriptUpdatesBaseUrl . "certs/" . $FileName . $ScriptUpdatesUrlSuffix) \
+ dst-path=$FileName as-value;
+ $WaitForFile $FileName;
} on-error={
- $LogPrintExit2 warning $0 ("Failed importing certificate with " . \
- "CommonName \"" . $CommonName . "\"!") false;
- :return false;
+ $LogPrint warning $0 ("Failed downloading certificate with CommonName '" . $CommonName . \
+ "' from repository! Trying fallback to mkcert.org...");
+ :do {
+ :if ([ :len [ /certificate/find where common-name="ISRG Root X1" ] ] = 0) do={
+ $LogPrint error $0 ("Required certificate is not available.");
+ :return false;
+ }
+ /tool/fetch check-certificate=yes-without-crl http-header-field=({ [ $FetchUserAgentStr $0 ] }) \
+ "https://mkcert.org/generate/" http-data=[ :serialize to=json ({ $CommonName }) ] \
+ dst-path=$FileName as-value;
+ $WaitForFile $FileName;
+ :if ([ /file/get $FileName size ] = 0) do={
+ $RmFile $FileName;
+ :error false;
+ }
+ } on-error={
+ $LogPrint warning $0 ("Failed downloading certificate with CommonName '" . $CommonName . "'!");
+ :return false;
+ }
}
+
+ /certificate/import file-name=$FileName passphrase="" as-value;
:delay 1s;
+ $RmFile $FileName;
+
+ :if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
+ /certificate/remove [ find where name~("^" . $FileName . "_[0-9]+\$") ];
+ $LogPrint warning $0 ("Certificate with CommonName '" . $CommonName . "' still unavailable!");
+ :return false;
+ }
+
+ :foreach Cert in=[ /certificate/find where name~("^" . $FileName . "_[0-9]+\$") ] do={
+ $CertificateNameByCN [ /certificate/get $Cert common-name ];
+ }
:return true;
}
# name a certificate by its common-name
:set CertificateNameByCN do={
- :local CommonName [ :tostr $1 ];
+ :local Match [ :tostr $1 ];
- :global CharacterReplace;
+ :global CleanName;
+ :global LogPrint;
+
+ :local Cert ([ /certificate/find where (common-name=$Match or fingerprint=$Match or name=$Match) ]->0);
+ :if ([ :len $Cert ] = 0) do={
+ $LogPrint warning $0 ("No matching certificate found.");
+ :return false;
+ }
+ :local CommonName [ /certificate/get $Cert common-name ];
+ /certificate/set $Cert name=[ $CleanName $CommonName ];
+ :return true;
+}
- :local Cert [ /certificate/find where common-name=$CommonName ];
- /certificate/set $Cert \
- name=[ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $CommonName "'" "-" ] " " "-" ] "---" "-" ];
+# multiply given character(s)
+:set CharacterMultiply do={
+ :local Return "";
+ :for I from=1 to=$2 do={
+ :set Return ($Return . $1);
+ }
+ :return $Return;
}
# character replace
@@ -193,34 +266,70 @@
:return $Path;
}
+# clean name for DNS, file and more
+:set CleanName do={
+ :local Input [ :tostr $1 ];
+
+ :local Return "";
+
+ :for I from=0 to=([ :len $Input ] - 1) do={
+ :local Char [ :pick $Input $I ];
+ :if ([ :typeof [ find "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" $Char ] ] = "nil") do={
+ :do {
+ :if ([ :len $Return ] = 0) do={
+ :error true;
+ }
+ :if ([ :pick $Return ([ :len $Return ] - 1) ] = "-") do={
+ :error true;
+ }
+ :set Char "-";
+ } on-error={
+ :set Char "";
+ }
+ }
+ :set Return ($Return . $Char);
+ }
+ :return $Return;
+}
+
# get readable device info
:set DeviceInfo do={
+ :global CommitId;
+ :global CommitInfo;
:global ExpectedConfigVersion;
:global Identity;
:global IfThenElse;
:global FormatLine;
+ :local License [ /system/license/get ];
:local Resource [ /system/resource/get ];
:local RouterBoard;
:do {
:set RouterBoard [[ :parse "/system/routerboard/get" ]];
} on-error={ }
- :local License [ /system/license/get ];
+ :local Snmp [ /snmp/get ];
:local Update [ /system/package/update/get ];
:return ( \
[ $FormatLine "Hostname" $Identity ] . "\n" . \
- [ $FormatLine "Board name" ($Resource->"board-name") ] . "\n" . \
- [ $FormatLine "Architecture" ($Resource->"architecture-name") ] . "\n" . \
+ [ $IfThenElse ([ :len ($Snmp->"location") ] > 0) \
+ ([ $FormatLine "Location" ($Snmp->"location") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($Snmp->"contact") ] > 0) \
+ ([ $FormatLine "Contact" ($Snmp->"contact") ] . "\n") ] . \
+ "Hardware:\n" . \
+ [ $FormatLine " Board" ($Resource->"board-name") ] . "\n" . \
+ [ $FormatLine " Arch" ($Resource->"architecture-name") ] . "\n" . \
[ $IfThenElse ($RouterBoard->"routerboard" = true) \
- ([ $FormatLine "Model" ($RouterBoard->"model") ] . \
+ ([ $FormatLine " Model" ($RouterBoard->"model") ] . \
[ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
(" " . $RouterBoard->"revision") ] . "\n" . \
- [ $FormatLine "Serial number" ($RouterBoard->"serial-number") ] . "\n") ] . \
- [ $IfThenElse ([ :len ($License->"level") ] > 0) \
- ([ $FormatLine "License" ($License->"level") ] . "\n") ] . \
+ [ $FormatLine " Serial" ($RouterBoard->"serial-number") ] . "\n") ] . \
+ [ $IfThenElse ([ :len ($License->"nlevel") ] > 0) \
+ ([ $FormatLine " License" ("level " . ($License->"nlevel")) ] . "\n") ] . \
"RouterOS:\n" . \
+ [ $IfThenElse ([ :len ($License->"level") ] > 0) \
+ ([ $FormatLine " License" ("level " . ($License->"level")) ] . "\n") ] . \
[ $FormatLine " Channel" ($Update->"channel") ] . "\n" . \
[ $FormatLine " Installed" ($Update->"installed-version") ] . "\n" . \
[ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \
@@ -230,16 +339,14 @@
$RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \
([ $FormatLine " Firmware" ($RouterBoard->"current-firmware") ] . "\n") ] . \
"RouterOS-Scripts:\n" . \
+ [ $IfThenElse ($CommitId != "unknown") \
+ ([ $FormatLine " Commit" ($CommitInfo . "/" . [ :pick $CommitId 0 8 ]) ] . "\n") ] . \
[ $FormatLine " Version" $ExpectedConfigVersion ]);
}
# convert line endings, DOS -> UNIX
:set Dos2Unix do={
- :local Input [ :tostr $1 ];
-
- :global CharacterReplace;
-
- :return [ $CharacterReplace $Input ("\r\n") ("\n") ];
+ :return [ :tolf [ :tostr $1 ] ];
}
# download package from upgrade server
@@ -251,8 +358,9 @@
:global CertificateAvailable;
:global CleanFilePath;
- :global LogPrintExit2;
+ :global LogPrint;
:global MkDir;
+ :global RmFile;
:global WaitForFile;
:if ([ :len $PkgName ] = 0) do={ :return false; }
@@ -266,22 +374,23 @@
:local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];
:if ([ $MkDir $PkgDir ] = false) do={
- $LogPrintExit2 warning $0 ("Failed creating directory, not downloading package.") false;
+ $LogPrint warning $0 ("Failed creating directory, not downloading package.");
:return false;
}
:if ([ :len [ /file/find where name=$PkgDest type="package" ] ] > 0) do={
- $LogPrintExit2 info $0 ("Package file " . $PkgName . " already exists.") false;
+ $LogPrint info $0 ("Package file " . $PkgName . " already exists.");
:return true;
}
- :if ([ $CertificateAvailable "R3" ] = false) do={
- $LogPrintExit2 error $0 ("Downloading required certificate failed.") true;
+ :if ([ $CertificateAvailable "ISRG Root X1" ] = false) do={
+ $LogPrint error $0 ("Downloading required certificate failed.");
+ :return false;
}
:local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile);
- $LogPrintExit2 info $0 ("Downloading package file '" . $PkgName . "'...") false;
- $LogPrintExit2 debug $0 ("... from url: " . $Url) false;
+ $LogPrint info $0 ("Downloading package file '" . $PkgName . "'...");
+ $LogPrint debug $0 ("... from url: " . $Url);
:local Retry 3;
:while ($Retry > 0) do={
:do {
@@ -292,14 +401,14 @@
:return true;
}
} on-error={
- $LogPrintExit2 debug $0 ("Downloading package file failed.") false;
+ $LogPrint debug $0 ("Downloading package file failed.");
}
- /file/remove [ find where name=$PkgDest ];
+ $RmFile $PkgDest;
:set Retry ($Retry - 1);
}
- $LogPrintExit2 warning $0 ("Downloading package file '" . $PkgName . "' failed.") false;
+ $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed.");
:return false;
}
@@ -313,6 +422,7 @@
:if ([ :typeof $1 ] = "time") do={
:return [ $IfThenElse ($1 > 0s) $1 $2 ];
}
+ # this works for boolean values, literal ones with parentheses
:return [ $IfThenElse ([ :len [ :tostr $1 ] ] > 0) $1 $2 ];
}
@@ -338,17 +448,96 @@
:return $Return;
}
+# simple macro to print error message on unintentional error
+:set ExitError do={
+ :local ExitOK [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+
+ :global IfThenElse;
+ :global LogPrint;
+
+ :if ($ExitOK = "false") do={
+ $LogPrint error $Name ([ $IfThenElse ([ :pick $Name 0 1 ] = "\$") \
+ "Function" "Script" ] . " '" . $Name . "' exited with error.");
+ }
+}
+
+# fetch huge data to file, read in chunks
+:set FetchHuge do={
+ :local ScriptName [ :tostr $1 ];
+ :local Url [ :tostr $2 ];
+ :local CheckCert [ :tostr $3 ];
+
+ :global CleanName;
+ :global FetchUserAgentStr;
+ :global GetRandom20CharAlNum;
+ :global IfThenElse;
+ :global LogPrint;
+ :global MkDir;
+ :global RmDir;
+ :global RmFile;
+ :global WaitForFile;
+
+ :set CheckCert [ $IfThenElse ($CheckCert = "false") "no" "yes-without-crl" ];
+
+ :local DirName ("tmpfs/" . [ $CleanName $ScriptName ]);
+ :if ([ $MkDir $DirName ] = false) do={
+ $LogPrint error $0 ("Failed creating directory!");
+ :return false;
+ }
+
+ :local FileName ($DirName . "/" . [ $CleanName $0 ] . "-" . [ $GetRandom20CharAlNum ]);
+ :do {
+ /tool/fetch check-certificate=$CheckCert $Url dst-path=$FileName \
+ http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value;
+ } on-error={
+ :if ([ $WaitForFile $FileName 500ms ] = true) do={
+ $RmFile $FileName;
+ }
+ $LogPrint debug $0 ("Failed downloading from: " . $Url);
+ $RmDir $DirName;
+ :return false;
+ }
+ $WaitForFile $FileName;
+
+ :local FileSize [ /file/get $FileName size ];
+ :local Return "";
+ :local VarSize 0;
+ :while ($VarSize != $FileSize) do={
+ :set Return ($Return . ([ /file/read offset=$VarSize chunk-size=32768 file=$FileName as-value ]->"data"));
+ :set FileSize [ /file/get $FileName size ];
+ :set VarSize [ :len $Return ];
+ :if ($VarSize > $FileSize) do={
+ :delay 100ms;
+ }
+ }
+ $RmDir $DirName;
+ :return $Return;
+}
+
+# generate user agent string for fetch
+:set FetchUserAgentStr do={
+ :local Caller [ :tostr $1 ];
+
+ :local Resource [ /system/resource/get ];
+
+ :return ("User-Agent: Mikrotik/" . $Resource->"version" . " " . \
+ $Resource->"architecture-name" . " " . $Caller . "/Fetch (https://rsc.eworm.de/)");
+}
+
# format a line for output
:set FormatLine do={
:local Key [ :tostr $1 ];
:local Value [ :tostr $2 ];
:local Indent [ :tonum $3 ];
- :local Spaces " ";
+ :local Spaces;
:local Return "";
+ :global CharacterMultiply;
:global EitherOr;
:set Indent [ $EitherOr $Indent 16 ];
+ :local Spaces [ $CharacterMultiply " " $Indent ];
:if ([ :len $Key ] > 0) do={ :set Return ($Key . ":"); }
:if ([ :len $Key ] > ($Indent - 2)) do={
@@ -383,15 +572,16 @@
:global CertificateAvailable;
:global IsMacLocallyAdministered;
- :global LogPrintExit2;
+ :global LogPrint;
:if ([ $IsMacLocallyAdministered $Mac ] = true) do={
:return "locally administered";
}
:do {
- :if ([ $CertificateAvailable "R3" ] = false) do={
- $LogPrintExit2 warning $0 ("Downloading required certificate failed.") true;
+ :if ([ $CertificateAvailable "GTS Root R4" ] = false) do={
+ $LogPrint warning $0 ("Downloading required certificate failed.");
+ :error false;
}
:local Vendor ([ /tool/fetch check-certificate=yes-without-crl \
("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data");
@@ -400,9 +590,9 @@
: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;
+ $LogPrint debug $0 ("The mac vendor is not known in database.");
} on-error={
- $LogPrintExit2 warning $0 ("Failed getting mac vendor.") false;
+ $LogPrint warning $0 ("Failed getting mac vendor.");
}
:return "unknown vendor";
}
@@ -452,16 +642,48 @@
# 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);
+ :global HexToNum;
+
+ :if ([ :pick $Input 0 ] = "*") do={
+ :return [ $HexToNum [ :pick $Input 1 [ :len $Input ] ] ];
}
- :return $Return;
+ :return [ :tonum ("0x" . $Input) ];
+}
+
+# return human readable number
+:set HumanReadableNum do={
+ :local Input [ :tonum $1 ];
+ :local Base [ :tonum $2 ];
+
+ :global EitherOr;
+ :global IfThenElse;
+
+ :local Prefix "kMGTPE";
+ :local Pow 1;
+
+ :set Base [ $EitherOr $Base 1024 ];
+ :local Bin [ $IfThenElse ($Base = 1024) "i" "" ];
+
+ :if ($Input < $Base) do={
+ :return $Input;
+ }
+
+ :for I from=0 to=[ :len $Prefix ] do={
+ :set Pow ($Pow * $Base);
+ :if ($Input / $Base < $Pow) do={
+ :set Prefix [ :pick $Prefix $I ];
+ :local Tmp1 ($Input * 100 / $Pow);
+ :local Tmp2 ($Tmp1 / 100);
+ :if ($Tmp2 >= 100) do={
+ :return ($Tmp2 . $Prefix . $Bin);
+ }
+ :return ($Tmp2 . "." . \
+ [ :pick $Tmp1 [ :len $Tmp2 ] ([ :len $Tmp1 ] - [ :len $Tmp2 ] + 1) ] . \
+ $Prefix . $Bin);
+ }
+ }
}
# mimic conditional/ternary operator (condition ? consequent : alternative)
@@ -482,8 +704,6 @@
# check if DNS is resolving
:set IsDNSResolving do={
- :global CharacterReplace;
-
:do {
:resolve "low-ttl.eworm.de";
} on-error={
@@ -523,7 +743,7 @@
:global IsTimeSyncCached;
:global IsTimeSyncResetNtp;
- :global LogPrintExit2;
+ :global LogPrintOnce;
:if ($IsTimeSyncCached = true) do={
:return true;
@@ -535,14 +755,15 @@
:return true;
}
+ :local Uptime [ /system/resource/get uptime ];
:if ([ :typeof $IsTimeSyncResetNtp ] = "nothing") do={
- :set IsTimeSyncResetNtp 0s;
+ :set IsTimeSyncResetNtp $Uptime;
}
- :local Uptime [ /system/resource/get uptime ];
:if ($Uptime - $IsTimeSyncResetNtp < 3m) do={
:return false;
}
+ $LogPrintOnce warning $0 ("The ntp client is configured, but did not sync.");
:set IsTimeSyncResetNtp $Uptime;
/system/ntp/client/set enabled=no;
:delay 20ms;
@@ -552,7 +773,7 @@
:if ([ /system/license/get ]->"level" = "free" || \
[ /system/resource/get ]->"board-name" = "x86") do={
- $LogPrintExit2 debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86.") false;
+ $LogPrintOnce debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86.");
:return true;
}
@@ -564,16 +785,15 @@
:return false;
}
- $LogPrintExit2 debug $0 ("No time source configured! Returning gracefully...") false;
+ $LogPrintOnce debug $0 ("No time source configured! Returning gracefully...");
:return true;
}
-# log and print with same text, optionally exit
-:set LogPrintExit2 do={
+# log and print with same text
+:set LogPrint do={
:local Severity [ :tostr $1 ];
:local Name [ :tostr $2 ];
:local Message [ :tostr $3 ];
- :local Exit [ :tostr $4 ];
:global PrintDebug;
:global PrintDebugOverride;
@@ -606,37 +826,80 @@
:if ($Severity != "debug" || $Debug = true) do={
:put ([ $PrintSeverity $Severity ] . ": " . $Message);
}
+}
- :if ($Exit = "true") do={
- :error ("Hard error to exit.");
+# log and print, once until reboot
+:set LogPrintOnce do={
+ :local Severity [ :tostr $1 ];
+ :local Name [ :tostr $2 ];
+ :local Message [ :tostr $3 ];
+
+ :global LogPrint;
+
+ :global LogPrintOnceMessages;
+
+ :if ([ :typeof $LogPrintOnceMessages ] = "nothing") do={
+ :set LogPrintOnceMessages ({});
+ }
+
+ :if ($LogPrintOnceMessages->$Message = 1) do={
+ :return false;
}
+
+ :if ([ :len [ /log/find where message=($Name . ": " . $Message) ] ] > 0) do={
+ $LogPrint warning $0 \
+ ("The message is already in log, scripting subsystem may have crashed before!");
+ }
+
+ :set ($LogPrintOnceMessages->$Message) 1;
+ $LogPrint $Severity $Name $Message;
+ :return true;
+}
+
+# The function $LogPrintVerbose is declared, but has no code, intentionally.
+# https://rsc.eworm.de/DEBUG.md#verbose-output
+
+# get max value
+:set MAX do={
+ :if ($1 > $2) do={ :return $1; }
+ :return $2;
+}
+
+# get min value
+:set MIN do={
+ :if ($1 < $2) do={ :return $1; }
+ :return $2;
}
# create directory
:set MkDir do={
:local Path [ :tostr $1 ];
- :global CharacterReplace;
:global CleanFilePath;
- :global GetRandom20CharAlNum;
- :global LogPrintExit2;
+ :global LogPrint;
+ :global RmDir;
:global WaitForFile;
:local MkTmpfs do={
- :global LogPrintExit2;
+ :global LogPrint;
:global WaitForFile;
- :if ([ :len [ /disk/find where slot=tmpfs type=tmpfs ] ] = 1) do={
+ :local TmpFs [ /disk/find where slot=tmpfs type=tmpfs ];
+ :if ([ :len $TmpFs ] = 1) do={
+ :if ([ /disk/get $TmpFs disabled ] = true) do={
+ $LogPrint info $0 ("The tmpfs is disabled, enabling.");
+ /disk/enable $TmpFs;
+ }
:return true;
}
- $LogPrintExit2 info $0 ("Creating disk of type tmpfs.") false;
- /file/remove [ find where name="tmpfs" type="directory" ];
+ $LogPrint info $0 ("Creating disk of type tmpfs.");
+ $RmDir "tmpfs";
:do {
/disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3);
$WaitForFile "tmpfs";
} on-error={
- $LogPrintExit2 warning $0 ("Creating disk of type tmpfs failed!") false;
+ $LogPrint warning $0 ("Creating disk of type tmpfs failed!");
:return false;
}
:return true;
@@ -648,7 +911,10 @@
:return true;
}
+ $LogPrint debug $0 ("Making directory: " . $Path);
+
:if ([ :len [ /file/find where name=$Path type="directory" ] ] = 1) do={
+ $LogPrint debug $0 ("... which already exists.");
:return true;
}
@@ -659,12 +925,10 @@
}
:do {
- :local File ($Path . "/file");
- /file/add name=$File;
- $WaitForFile $File;
- /file/remove $File;
+ /file/add type="directory" name=$Path;
+ $WaitForFile $Path;
} on-error={
- $LogPrintExit2 warning $0 ("Making directory '" . $Path . "' failed!") false;
+ $LogPrint warning $0 ("Making directory '" . $Path . "' failed!");
:return false;
}
@@ -680,15 +944,6 @@
:set ParseDate do={
:local Date [ :tostr $1 ];
- :if ([ :pick $Date 4 5 ] != "-") do={
- :local Months { "jan"=1; "feb"=2; "mar"=3; "apr"=4; "may"=5; "jun"=6;
- "jul"=7; "aug"=8; "sep"=9; "oct"=10; "nov"=11; "dec"=12 };
-
- :return ({ "year"=[ :tonum [ :pick $Date 7 11 ] ];
- "month"=($Months->[ :pick $Date 0 3 ]);
- "day"=[ :tonum [ :pick $Date 4 6 ] ] });
- }
-
:return ({ "year"=[ :tonum [ :pick $Date 0 4 ] ];
"month"=[ :tonum [ :pick $Date 5 7 ] ];
"day"=[ :tonum [ :pick $Date 8 10 ] ] });
@@ -697,14 +952,24 @@
# parse key value store
:set ParseKeyValueStore do={
:local Source $1;
+
+ :if ([ :pick $Source 0 1 ] = "{") do={
+ :do {
+ :return [ :deserialize from=json $Source ];
+ } on-error={ }
+ }
+
: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 ] ];
+ :local Key [ :pick $KeyValue 0 [ :find $KeyValue "=" ] ];
+ :local Value [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
+ :if ($Value="true") do={ :set Value true; }
+ :if ($Value="false") do={ :set Value false; }
+ :set ($Result->$Key) $Value;
} else={
:set ($Result->$KeyValue) true;
}
@@ -714,24 +979,34 @@
# print lines with trailing carriage return
:set PrettyPrint do={
- :local Input [ :tostr $1 ];
+ :put [ :tocrlf [ :tostr $1 ] ];
+}
- :global Unix2Dos;
+# strip protocol from from url string
+:set ProtocolStrip do={
+ :local Input [ :tostr $1 ];
- :put [ $Unix2Dos $Input ];
+ :local Pos [ :find $Input "://" ];
+ :if ([ :typeof $Pos ] = "nil") do={
+ :return $Input;
+ }
+ :return [ :pick $Input ($Pos + 3) [ :len $Input ] ];
}
# delay a random amount of seconds
:set RandomDelay do={
+ :local Time [ :tonum $1 ];
+ :local Unit [ :tostr $2 ];
+
:global EitherOr;
:global GetRandomNumber;
+ :global MAX;
- :delay ([ $GetRandomNumber $1 ] . [ $EitherOr $2 "s" ]);
-}
+ :if ($Time = 0) do={
+ :return false;
+ }
-# read input from user
-:set Read do={
- :return;
+ :delay ([ $MAX 10 [ $GetRandomNumber ([ :tonsec [ :totime ($Time . [ $EitherOr $Unit "s" ]) ] ] / 1000000) ] ] . "ms");
}
# check for required RouterOS version
@@ -741,29 +1016,90 @@
:local Warn [ :tostr $3 ];
:global IfThenElse;
- :global LogPrintExit2;
+ :global LogPrint;
:global VersionToNum;
:if (!($Required ~ "^\\d+\\.\\d+((alpha|beta|rc|\\.)\\d+|)\$")) do={
- $LogPrintExit2 error $0 ("No valid RouterOS version: " . $Required) false;
+ $LogPrint error $0 ("No valid RouterOS version: " . $Required);
:return false;
}
: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;
+ $LogPrint warning $0 ("This " . [ $IfThenElse ([ :pick $Caller 0 ] = ("\$")) "function" "script" ] . \
+ " '" . $Caller . "' (at least specific functionality) requires RouterOS " . $Required . ". Please update!");
}
:return false;
}
:return true;
}
+# remove directory
+:set RmDir do={
+ :local DirName [ :tostr $1 ];
+
+ :global LogPrint;
+
+ $LogPrint debug $0 ("Removing directory: ". $DirName);
+
+ :if ([ :len [ /file/find where name=$DirName type!=directory ] ] > 0) do={
+ $LogPrint error $0 ("Directory '" . $DirName . "' is not a directory.");
+ :return false;
+ }
+
+ :local Dir [ /file/find where name=$DirName type=directory ];
+ :if ([ :len $Dir ] = 0) do={
+ $LogPrint debug $0 ("... which does not exist.");
+ :return true;
+ }
+
+ :do {
+ /file/remove $Dir;
+ } on-error={
+ $LogPrint error $0 ("Removing directory '" . $DirName . "' (" . $Dir . ") failed.");
+ :return false;
+ }
+ :return true;
+}
+
+# remove file
+:set RmFile do={
+ :local FileName [ :tostr $1 ];
+
+ :global LogPrint;
+
+ $LogPrint debug $0 ("Removing file: ". $FileName);
+
+ :if ([ :len [ /file/find where name=$FileName (type=directory or type=disk) ] ] > 0) do={
+ $LogPrint error $0 ("File '" . $FileName . "' is not a file.");
+ :return false;
+ }
+
+ :local File [ /file/find where name=$FileName !(type=directory or type=disk) ];
+ :if ([ :len $File ] = 0) do={
+ $LogPrint debug $0 ("... which does not exist.");
+ :return true;
+ }
+
+ :do {
+ /file/remove $File;
+ } on-error={
+ $LogPrint error $0 ("Removing file '" . $FileName . "' (" . $File . ") failed.");
+ :return false;
+ }
+ :return true;
+}
+
# check if script is run from terminal
:set ScriptFromTerminal do={
:local Script [ :tostr $1 ];
- :global LogPrintExit2;
+ :global LogPrint;
+ :global ScriptLock;
+
+ :if ([ $ScriptLock $Script ] = false) do={
+ :return false;
+ }
:foreach Job in=[ /system/script/job/find where script=$Script ] do={
:set Job [ /system/script/job/get $Job ];
@@ -771,162 +1107,202 @@
: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;
+ $LogPrint debug $0 ("Script " . $Script . " started from terminal.");
:return true;
}
}
- $LogPrintExit2 debug $0 ("Script " . $Script . " NOT started from terminal.") false;
+ $LogPrint debug $0 ("Script " . $Script . " NOT started from terminal.");
:return false;
}
# install new scripts, update existing scripts
-:set ScriptInstallUpdate do={
+:set ScriptInstallUpdate do={ :do {
:local Scripts [ :toarray $1 ];
:local NewComment [ :tostr $2 ];
+ :global CommitId;
+ :global CommitInfo;
:global ExpectedConfigVersion;
- :global FetchUserAgent;
:global Identity;
:global IDonate;
:global NoNewsAndChangesNotification;
- :global NotificationsWithSymbols;
:global ScriptUpdatesBaseUrl;
- :global ScriptUpdatesFetch;
+ :global ScriptUpdatesCRLF;
:global ScriptUpdatesUrlSuffix;
:global CertificateAvailable;
:global EitherOr;
+ :global FetchUserAgentStr;
:global Grep;
:global IfThenElse;
- :global LogPrintExit2;
+ :global LogPrint;
+ :global LogPrintOnce;
:global ParseKeyValueStore;
:global RequiredRouterOS;
:global SendNotification2;
:global SymbolForNotification;
:global ValidateSyntax;
- :if ([ $CertificateAvailable "R3" ] = false) do={
- $LogPrintExit2 warning $0 ("Downloading certificate failed, trying without.") false;
- }
-
- :if ([ $CertificateAvailable "E1" ] = false) do={
- $LogPrintExit2 warning $0 ("Downloading certificate failed, trying without.") false;
+ :if ([ $CertificateAvailable "ISRG Root X2" ] = false) do={
+ $LogPrint warning $0 ("Downloading certificate failed, trying without.");
}
:foreach Script in=$Scripts do={
:if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
- $LogPrintExit2 info $0 ("Adding new script: " . $Script) false;
+ $LogPrint info $0 ("Adding new script: " . $Script);
/system/script/add name=$Script owner=$Script source="#!rsc by RouterOS\n" comment=$NewComment;
}
}
+ :local CommitIdBefore $CommitId;
:local ExpectedConfigVersionBefore $ExpectedConfigVersion;
:local ReloadGlobalFunctions false;
:local ReloadGlobalConfig false;
+ :local DeviceMode [ /system/device-mode/get ];
+
+ :local CheckSums ({});
+ :do {
+ :local Url ($ScriptUpdatesBaseUrl . "checksums.json" . $ScriptUpdatesUrlSuffix);
+ $LogPrint debug $0 ("Fetching checksums from url: " . $Url);
+ :set CheckSums [ :deserialize from=json ([ /tool/fetch check-certificate=yes-without-crl \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ]->"data") ];
+ } on-error={ }
:foreach Script in=[ /system/script/find where source~"^#!rsc by RouterOS\r?\n" ] do={
:local ScriptVal [ /system/script/get $Script ];
- :local ScriptFile [ /file/find where name=("script-updates/" . $ScriptVal->"name") . ".rsc" ];
+ :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"comment") ];
:local SourceNew;
- :if ([ :len $ScriptFile ] > 0) do={
- :set SourceNew [ /file/get $ScriptFile contents ];
- /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;
+ $LogPrint warning $0 ("Policies differ for script '" . $ScriptVal->"name" . \
+ "' and its scheduler '" . $SchedulerVal->"name" . "'!");
}
}
- :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" . ".rsc" . $UrlSuffix);
-
- $LogPrintExit2 debug $0 ("Fetching script '" . $ScriptVal->"name" . "' from url: " . $Url) false;
- :local Result [ /tool/fetch check-certificate=yes-without-crl http-header-field=$FetchUserAgent \
- $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;
- }
+ :do {
+ :if ($ScriptInfo->"ignore" = true) do={
+ $LogPrint debug $0 ("Ignoring script '" . $ScriptVal->"name" . "', as requested.");
+ :error true;
+ }
+
+ :local CheckSum ($CheckSums->($ScriptVal->"name"));
+ :if ([ :len ($ScriptInfo->"base-url") ] = 0 && [ :len ($ScriptInfo->"url-suffix") ] = 0 && \
+ [ :convert transform=md5 to=hex [ :tolf ($ScriptVal->"source") ] ] = $CheckSum) do={
+ $LogPrint debug $0 ("Checksum for script '" . $ScriptVal->"name" . "' matches, ignoring.");
+ :error true;
+ }
+
+ :if ([ :len ($ScriptInfo->"certificate") ] > 0) do={
+ :if ([ $CertificateAvailable ($ScriptInfo->"certificate") ] = false) do={
+ $LogPrint warning $0 ("Downloading certificate failed, trying without.");
}
}
- }
- :if ([ :len $SourceNew ] > 0) do={
- :if ($SourceNew != $ScriptVal->"source") do={
- :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
- :local Required ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version");
- :if ([ $RequiredRouterOS $0 [ $EitherOr $Required "0.0" ] false ] = true) do={
- :if ([ $ValidateSyntax $SourceNew ] = true) do={
- $LogPrintExit2 info $0 ("Updating script: " . $ScriptVal->"name") false;
- /system/script/set owner=($ScriptVal->"name") source=$SourceNew $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 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
- $Required . ", which is not met by your installation. Ignoring!") false;
- }
+ :do {
+ :local BaseUrl [ $EitherOr ($ScriptInfo->"base-url") $ScriptUpdatesBaseUrl ];
+ :local UrlSuffix [ $EitherOr ($ScriptInfo->"url-suffix") $ScriptUpdatesUrlSuffix ];
+ :local Url ($BaseUrl . $ScriptVal->"name" . ".rsc" . $UrlSuffix);
+ $LogPrint debug $0 ("Fetching script '" . $ScriptVal->"name" . "' from url: " . $Url);
+ :local Result [ /tool/fetch check-certificate=yes-without-crl \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) $Url output=user as-value ];
+ :if ($Result->"status" = "finished") do={
+ :set SourceNew [ :tolf ($Result->"data") ];
+ }
+ } on-error={
+ :if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={
+ $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . \
+ "', removing dummy. Typo on installation?");
+ /system/script/remove $Script;
} else={
- $LogPrintExit2 warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
- "' is not valid (missing shebang). Ignoring!") false;
+ $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!");
}
- } else={
- $LogPrintExit2 debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.") false;
+ :error false;
}
- } else={
- $LogPrintExit2 debug $0 ("No update for script '" . $ScriptVal->"name" . "'.") false;
- }
+
+ :if ([ :len $SourceNew ] = 0) do={
+ $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'.");
+ :error false;
+ }
+
+ :local SourceCRLF [ :tocrlf $SourceNew ];
+ :if ($SourceNew = $ScriptVal->"source" || $SourceCRLF = $ScriptVal->"source") do={
+ $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.");
+ :error false;
+ }
+
+ :if ([ :pick $SourceNew 0 18 ] != "#!rsc by RouterOS\n") do={
+ $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
+ "' is not valid (missing shebang). Ignoring!");
+ :error false;
+ }
+
+ :local RequiredROS ([ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires RouterOS, ") ] ]->"version");
+ :if ([ $RequiredRouterOS $0 [ $EitherOr $RequiredROS "0.0" ] false ] = false) do={
+ $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
+ $RequiredROS . ", which is not met by your installation. Ignoring!");
+ :error false;
+ }
+
+ :local RequiredDM [ $ParseKeyValueStore [ $Grep $SourceNew ("\23 requires device-mode, ") ] ];
+ :local MissingDM ({});
+ :foreach Feature,Value in=$RequiredDM do={
+ :if ([ :typeof ($DeviceMode->$Feature) ] = "bool" && ($DeviceMode->$Feature) = false) do={
+ :set MissingDM ($MissingDM, $Feature);
+ }
+ }
+ :if ([ :len $MissingDM ] > 0) do={
+ $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires disabled " . \
+ "device-mode features (" . [ :tostr $MissingDM ] . "). Ignoring!");
+ :error false;
+ }
+
+ :if ([ $ValidateSyntax $SourceNew ] = false) do={
+ $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . "' failed! Ignoring!");
+ :error false;
+ }
+
+ $LogPrint info $0 ("Updating script: " . $ScriptVal->"name");
+ /system/script/set owner=($ScriptVal->"name") \
+ source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script;
+ :if ($ScriptVal->"name" = "global-config") do={
+ :set ReloadGlobalConfig true;
+ }
+ :if ($ScriptVal->"name" = "global-functions" || $ScriptVal->"name" ~ ("^mod/.")) do={
+ :set ReloadGlobalFunctions true;
+ }
+ } on-error={ }
}
:if ($ReloadGlobalFunctions = true) do={
- $LogPrintExit2 info $0 ("Reloading global functions.") false;
+ $LogPrint info $0 ("Reloading global functions.");
:do {
/system/script/run global-functions;
} on-error={
- $LogPrintExit2 error $0 ("Reloading global functions failed!") false;
+ $LogPrint error $0 ("Reloading global functions failed!");
}
}
:if ($ReloadGlobalConfig = true) do={
- $LogPrintExit2 info $0 ("Reloading global configuration.") false;
+ $LogPrint info $0 ("Reloading global configuration.");
:do {
/system/script/run global-config;
} on-error={
- $LogPrintExit2 error $0 ("Reloading global configuration failed!" . \
- " Syntax error or missing overlay?") false;
+ $LogPrint error $0 ("Reloading global configuration failed!" . \
+ " Syntax error or missing overlay?");
}
}
+ :if ($CommitId != "unknown" && $CommitIdBefore != $CommitId) do={
+ $LogPrint info $0 ("Updated to commit: " . $CommitInfo . "/" . [ :pick $CommitId 0 8 ]);
+ }
+
:if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={
- $LogPrintExit2 warning $0 ("The configuration version decreased from " . \
+ $LogPrint warning $0 ("The configuration version decreased from " . \
$ExpectedConfigVersionBefore . " to " . $ExpectedConfigVersion . \
- ". Installed an older version?") false;
+ ". Installed an older version?");
}
:if ($ExpectedConfigVersionBefore < $ExpectedConfigVersion) do={
@@ -936,14 +1312,14 @@
:do {
:local Url ($ScriptUpdatesBaseUrl . "news-and-changes.rsc" . $ScriptUpdatesUrlSuffix);
- $LogPrintExit2 debug $0 ("Fetching news, changes and migration: " . $Url) false;
- :local Result [ /tool/fetch check-certificate=yes-without-crl http-header-field=$FetchUserAgent \
- $Url output=user as-value ];
+ $LogPrint debug $0 ("Fetching news, changes and migration: " . $Url);
+ :local Result [ /tool/fetch check-certificate=yes-without-crl \
+ http-header-field=({ [ $FetchUserAgentStr $0 ] }) $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;
+ $LogPrint warning $0 ("Failed fetching news, changes and migration!");
}
:if ([ :len $ChangeLogCode ] > 0) do={
@@ -951,43 +1327,49 @@
:do {
[ :parse $ChangeLogCode ];
} on-error={
- $LogPrintExit2 warning $0 ("The changelog failed to run!") false;
+ $LogPrint warning $0 ("The changelog failed to run!");
}
} else={
- $LogPrintExit2 warning $0 ("The changelog failed syntax validation!") false;
+ $LogPrint warning $0 ("The changelog failed syntax validation!");
}
}
: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;
+ :do {
+ :if ([ :typeof $Migration ] != "str") do={
+ $LogPrint debug $0 ("Migration code for change " . $I . " is not available.");
+ :error false;
}
- }
+
+ :if ([ $ValidateSyntax $Migration ] = false) do={
+ $LogPrint warning $0 ("Migration code for change " . $I . " failed syntax validation!");
+ :error false;
+ }
+
+ $LogPrint info $0 ("Applying migration for change " . $I . ": " . $Migration);
+ :do {
+ [ :parse $Migration ];
+ } on-error={
+ $LogPrint warning $0 ("Migration code for change " . $I . " failed to run!");
+ }
+ } on-error={ }
}
}
: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;
+ $LogPrint info $0 ($NotificationMessage);
: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;
+ [ $SymbolForNotification "pushpin" "*" ] . $Change);
+ $LogPrint info $0 ("Change " . $I . ": " . $Change);
}
} else={
:set NotificationMessage ($NotificationMessage . "\n\nNews and changes are not available.");
@@ -1001,7 +1383,7 @@
"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";
+ :set Link "https://rsc.eworm.de/#donate";
}
$SendNotification2 ({ origin=$0; \
@@ -1012,17 +1394,18 @@
:set GlobalConfigChanges;
:set GlobalConfigMigration;
}
-}
+} on-error={
+ :global ExitError; $ExitError false $0;
+} }
# lock script against multiple invocation
:set ScriptLock do={
- :local Script [ :tostr $1 ];
- :local DoReturn $2;
- :local WaitMax ([ :tonum $3 ] * 10);
+ :local Script [ :tostr $1 ];
+ :local WaitMax [ :totime $2 ];
:global GetRandom20CharAlNum;
:global IfThenElse;
- :global LogPrintExit2;
+ :global LogPrint;
:global ScriptLockOrder;
:if ([ :typeof $ScriptLockOrder ] = "nothing") do={
@@ -1107,16 +1490,22 @@
:set ($ScriptLockOrder->$Script) ({});
}
+ :if ([ :typeof $WaitMax ] = "nil" ) do={
+ :set WaitMax 0s;
+ }
+
:if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
- $LogPrintExit2 error $0 ("A script named '" . $Script . "' does not exist!") true;
+ $LogPrint error $0 ("A script named '" . $Script . "' does not exist!");
+ :error false;
}
:if ([ $JobCount $Script ] = 0) do={
- $LogPrintExit2 error $0 ("No script '" . $Script . "' is running!") true;
+ $LogPrint error $0 ("No script '" . $Script . "' is running!");
+ :error false;
}
:if ([ $TicketCount $Script ] >= [ $JobCount $Script ]) do={
- $LogPrintExit2 error $0 ("More tickets than running scripts '" . $Script . "', resetting!") false;
+ $LogPrint error $0 ("More tickets than running scripts '" . $Script . "', resetting!");
:set ($ScriptLockOrder->$Script) ({});
/system/script/job/remove [ find where script=$Script ];
}
@@ -1124,30 +1513,36 @@
:local MyTicket [ $GetRandom20CharAlNum 6 ];
$AddTicket $Script $MyTicket;
- :local WaitCount 0;
- :while ($WaitMax > $WaitCount && ([ $IsFirstTicket $Script $MyTicket ] = false || [ $TicketCount $Script ] < [ $JobCount $Script ])) do={
- :set WaitCount ($WaitCount + 1);
- :delay 100ms;
+ :local WaitInterval ($WaitMax / 20);
+ :local WaitTime $WaitMax;
+ :while ($WaitTime > 0 && \
+ ([ $IsFirstTicket $Script $MyTicket ] = false || \
+ [ $TicketCount $Script ] < [ $JobCount $Script ])) do={
+ :set WaitTime ($WaitTime - $WaitInterval);
+ :delay $WaitInterval;
}
- :if ([ $IsFirstTicket $Script $MyTicket ] = true && [ $TicketCount $Script ] = [ $JobCount $Script ]) do={
+ :if ([ $IsFirstTicket $Script $MyTicket ] = true && \
+ [ $TicketCount $Script ] = [ $JobCount $Script ]) do={
$RemoveTicket $Script $MyTicket;
$CleanupTickets $Script;
- :return false;
+ :return true;
}
$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;
+ $LogPrint debug $0 ("Script '" . $Script . "' started more than once" . \
+ [ $IfThenElse ($WaitTime < $WaitMax) " and timed out waiting for lock" "" ] . "...");
+ :return false;
}
# send notification via NotificationFunctions - expects at least two string arguments
-:set SendNotification do={
+:set SendNotification do={ :do {
:global SendNotification2;
- $SendNotification2 ({ subject=$1; message=$2; link=$3; silent=$4 });
-}
+ $SendNotification2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 });
+} on-error={
+ :global ExitError; $ExitError false $0;
+} }
# send notification via NotificationFunctions - expects one array argument
:set SendNotification2 do={
@@ -1164,9 +1559,15 @@
# return UTF-8 symbol for unicode name
:set SymbolByUnicodeName do={
+ :local Name [ :tostr $1 ];
+
+ :global LogPrintOnce;
+
:local Symbols {
"abacus"="\F0\9F\A7\AE";
"alarm-clock"="\E2\8F\B0";
+ "arrow-down"="\E2\AC\87";
+ "arrow-up"="\E2\AC\86";
"calendar"="\F0\9F\93\85";
"card-file-box"="\F0\9F\97\83";
"chart-decreasing"="\F0\9F\93\89";
@@ -1176,31 +1577,45 @@
"earth"="\F0\9F\8C\8D";
"fire"="\F0\9F\94\A5";
"floppy-disk"="\F0\9F\92\BE";
+ "gear"="\E2\9A\99";
+ "heart"="\E2\99\A5";
"high-voltage-sign"="\E2\9A\A1";
"incoming-envelope"="\F0\9F\93\A8";
+ "information"="\E2\84\B9";
+ "large-orange-circle"="\F0\9F\9F\A0";
+ "large-red-circle"="\F0\9F\94\B4";
"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";
+ "smiley-partying-face"="\F0\9F\A5\B3";
+ "smiley-smiling-face"="\E2\98\BA";
+ "smiley-winking-face-with-tongue"="\F0\9F\98\9C";
"sparkles"="\E2\9C\A8";
"speech-balloon"="\F0\9F\92\AC";
- "up-arrow"="\E2\AC\86";
+ "star"="\E2\AD\90";
"warning-sign"="\E2\9A\A0";
"white-heavy-check-mark"="\E2\9C\85"
}
- :return (($Symbols->$1) . "\EF\B8\8F");
+ :if ([ :len ($Symbols->$Name) ] = 0) do={
+ $LogPrintOnce warning $0 ("No symbol available for name '" . $Name . "'!");
+ :return "";
+ }
+
+ :return (($Symbols->$Name) . "\EF\B8\8F");
}
# return symbol for notification
:set SymbolForNotification do={
:global NotificationsWithSymbols;
:global SymbolByUnicodeName;
+ :global IfThenElse;
:if ($NotificationsWithSymbols != true) do={
- :return "";
+ :return [ $IfThenElse ([ :len $2 ] > 0) ([ :tostr $2 ] . " ") "" ];
}
:local Return "";
:foreach Symbol in=[ :toarray $1 ] do={
@@ -1211,12 +1626,7 @@
# convert line endings, UNIX -> DOS
:set Unix2Dos do={
- :local Input [ :tostr $1 ];
-
- :global CharacterReplace;
-
- :return [ $CharacterReplace [ $CharacterReplace $Input \
- ("\n") ("\r\n") ] ("\r\r\n") ("\r\n") ];
+ :return [ :tocrlf [ :tostr $1 ] ];
}
# url encoding
@@ -1267,7 +1677,7 @@
:global CharacterReplace;
:set Input [ $CharacterReplace $Input "." "," ];
- :foreach I in={ "alpha"; "beta"; "rc" } do={
+ :foreach I in={ "zero"; "alpha"; "beta"; "rc" } do={
:set Input [ $CharacterReplace $Input $I ("," . $I . ",") ];
}
@@ -1278,6 +1688,7 @@
:set Return ($Return + 0xff00);
:set Multi ($Multi / 0x100);
} else={
+ :if ($Value = "zero") do={ }
:if ($Value = "alpha") do={ :set Return ($Return + 0x3f00); }
:if ($Value = "beta") do={ :set Return ($Return + 0x5f00); }
:if ($Value = "rc") do={ :set Return ($Return + 0x7f00); }
@@ -1315,19 +1726,30 @@
:global CleanFilePath;
:global EitherOr;
+ :global MAX;
:set FileName [ $CleanFilePath $FileName ];
:local I 1;
- :local Delay ([ :totime [ $EitherOr $WaitTime 2s ] ] / 20);
+ :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 10);
:while ([ :len [ /file/find where name=$FileName ] ] = 0) do={
- :if ($I >= 20) do={
+ :if ($I >= 10) do={
:return false;
}
:delay $Delay;
:set I ($I + 1);
}
- :return true;
+
+ :while ([ :len [ /file/find where name=$FileName ] ] > 0) do={
+ :do {
+ /file/get $FileName;
+ :return true;
+ } on-error={ }
+ :delay $Delay;
+ :set Delay ($Delay * 3 / 2);
+ }
+
+ :return false;
}
# wait to be fully connected (default route is reachable, time is sync, DNS resolves)
@@ -1357,12 +1779,17 @@
:do {
/system/script/run $Script;
} on-error={
- $LogPrintExit2 error $0 ("Module '" . $ScriptVal->"name" . "' failed to run.") false;
+ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run.");
}
} else={
- $LogPrintExit2 error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.") false;
+ $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.");
}
}
+# Log success
+:local Resource [ /system/resource/get ];
+$LogPrintOnce info $ScriptName ("Loaded on " . $Resource->"board-name" . \
+ " with RouterOS " . $Resource->"version" . ".");
+
# signal we are ready
:set GlobalFunctionsReady true;