aboutsummaryrefslogtreecommitdiffstats
path: root/global-functions.rsc
diff options
context:
space:
mode:
Diffstat (limited to 'global-functions.rsc')
-rw-r--r--global-functions.rsc289
1 files changed, 220 insertions, 69 deletions
diff --git a/global-functions.rsc b/global-functions.rsc
index 47a69c4..f5fa5cb 100644
--- a/global-functions.rsc
+++ b/global-functions.rsc
@@ -1,18 +1,21 @@
#!rsc by RouterOS
# RouterOS script: global-functions
-# Copyright (c) 2013-2024 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.14
+# 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 ScriptName [ :jobname ];
-# expected configuration version
-:global ExpectedConfigVersion 131;
+# Git commit id & info, expected configuration version
+:global CommitId "unknown";
+:global CommitInfo "unknown";
+:global ExpectedConfigVersion 133;
# global variables not to be changed by user
:global GlobalFunctionsReady false;
@@ -32,6 +35,7 @@
:global DownloadPackage;
:global EitherOr;
:global EscapeForRegEx;
+:global ExitError;
:global FetchHuge;
:global FetchUserAgentStr;
:global FormatLine;
@@ -61,6 +65,8 @@
:global ProtocolStrip;
:global RandomDelay;
:global RequiredRouterOS;
+:global RmDir;
+:global RmFile;
:global ScriptFromTerminal;
:global ScriptInstallUpdate;
:global ScriptLock;
@@ -145,6 +151,7 @@
:global CleanName;
:global FetchUserAgentStr;
:global LogPrint;
+ :global RmFile;
:global WaitForFile;
$LogPrint info $0 ("Downloading and importing certificate with " . \
@@ -168,7 +175,7 @@
dst-path=$FileName as-value;
$WaitForFile $FileName;
:if ([ /file/get $FileName size ] = 0) do={
- /file/remove $FileName;
+ $RmFile $FileName;
:error false;
}
} on-error={
@@ -179,7 +186,7 @@
/certificate/import file-name=$FileName passphrase="" as-value;
:delay 1s;
- /file/remove [ find where name=$FileName ];
+ $RmFile $FileName;
:if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
/certificate/remove [ find where name~("^" . $FileName . "_[0-9]+\$") ];
@@ -279,6 +286,8 @@
# get readable device info
:set DeviceInfo do={
+ :global CommitId;
+ :global CommitInfo;
:global ExpectedConfigVersion;
:global Identity;
@@ -319,6 +328,8 @@
$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 ]);
}
@@ -338,6 +349,7 @@
:global CleanFilePath;
:global LogPrint;
:global MkDir;
+ :global RmFile;
:global WaitForFile;
:if ([ :len $PkgName ] = 0) do={ :return false; }
@@ -381,7 +393,7 @@
$LogPrint debug $0 ("Downloading package file failed.");
}
- /file/remove [ find where name=$PkgDest ];
+ $RmFile $PkgDest;
:set Retry ($Retry - 1);
}
@@ -425,11 +437,25 @@
: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 [ :tobool $3 ];
+ :local ScriptName [ :tostr $1 ];
+ :local Url [ :tostr $2 ];
+ :local CheckCert [ :tostr $3 ];
:global CleanName;
:global FetchUserAgentStr;
@@ -437,9 +463,11 @@
:global IfThenElse;
:global LogPrint;
:global MkDir;
+ :global RmDir;
+ :global RmFile;
:global WaitForFile;
- :set CheckCert [ $IfThenElse ($CheckCert = false) "no" "yes-without-crl" ];
+ :set CheckCert [ $IfThenElse ($CheckCert = "false") "no" "yes-without-crl" ];
:local DirName ("tmpfs/" . [ $CleanName $ScriptName ]);
:if ([ $MkDir $DirName ] = false) do={
@@ -453,10 +481,10 @@
http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value;
} on-error={
:if ([ $WaitForFile $FileName 500ms ] = true) do={
- /file/remove $FileName;
+ $RmFile $FileName;
}
$LogPrint debug $0 ("Failed downloading from: " . $Url);
- /file/remove $DirName;
+ $RmDir $DirName;
:return false;
}
$WaitForFile $FileName;
@@ -464,11 +492,15 @@
:local FileSize [ /file/get $FileName size ];
:local Return "";
:local VarSize 0;
- :while ($VarSize < $FileSize) do={
+ :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;
+ }
}
- /file/remove $DirName;
+ $RmDir $DirName;
:return $Return;
}
@@ -831,6 +863,7 @@
:global CleanFilePath;
:global LogPrint;
+ :global RmDir;
:global WaitForFile;
:local MkTmpfs do={
@@ -847,7 +880,7 @@
}
$LogPrint info $0 ("Creating disk of type tmpfs.");
- /file/remove [ find where name="tmpfs" type="directory" ];
+ $RmDir "tmpfs";
:do {
/disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3);
$WaitForFile "tmpfs";
@@ -864,7 +897,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;
}
@@ -875,10 +911,8 @@
}
:do {
- :local File ($Path . "/file");
- /file/add name=$File;
- $WaitForFile $File;
- /file/remove $File;
+ /file/add type="directory" name=$Path;
+ $WaitForFile $Path;
} on-error={
$LogPrint warning $0 ("Making directory '" . $Path . "' failed!");
:return false;
@@ -904,14 +938,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;
}
@@ -976,6 +1020,62 @@
: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 ];
@@ -1003,10 +1103,12 @@
}
# 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 Identity;
:global IDonate;
@@ -1039,9 +1141,11 @@
}
}
+ :local CommitIdBefore $CommitId;
:local ExpectedConfigVersionBefore $ExpectedConfigVersion;
:local ReloadGlobalFunctions false;
:local ReloadGlobalConfig false;
+ :local DeviceMode [ /system/device-mode/get ];
:foreach Script in=[ /system/script/find where source~"^#!rsc by RouterOS\r?\n" ] do={
:local ScriptVal [ /system/script/get $Script ];
@@ -1078,40 +1182,59 @@
}
}
- :if ([ :len $SourceNew ] > 0) do={
+ :do {
+ :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={
- :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={
- $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;
- }
- } else={
- $LogPrint warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \
- "' failed! Ignoring!");
- }
- } else={
- $LogPrintOnce warning $0 ("The script '" . $ScriptVal->"name" . "' requires RouterOS " . \
- $Required . ", which is not met by your installation. Ignoring!");
- }
- } else={
- $LogPrint warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
+ :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);
}
- } else={
- $LogPrint debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.");
}
- } else={
- $LogPrint debug $0 ("No update for script '" . $ScriptVal->"name" . "'.");
- }
+ :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={
@@ -1133,6 +1256,10 @@
}
}
+ :if ($CommitId != "unknown" && $CommitIdBefore != $CommitId) do={
+ $LogPrint info $0 ("Updated to commit: " . $CommitInfo . "/" . [ :pick $CommitId 0 8 ]);
+ }
+
:if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={
$LogPrint warning $0 ("The configuration version decreased from " . \
$ExpectedConfigVersionBefore . " to " . $ExpectedConfigVersion . \
@@ -1171,18 +1298,24 @@
: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={
- $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!");
- }
- } else={
+ :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={ }
}
}
@@ -1222,7 +1355,9 @@
:set GlobalConfigChanges;
:set GlobalConfigMigration;
}
-}
+} on-error={
+ :global ExitError; $ExitError false $0;
+} }
# lock script against multiple invocation
:set ScriptLock do={
@@ -1357,11 +1492,13 @@
}
# send notification via NotificationFunctions - expects at least two string arguments
-:set SendNotification do={
+:set SendNotification do={ :do {
:global SendNotification2;
$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={
@@ -1545,6 +1682,7 @@
:global CleanFilePath;
:global EitherOr;
+ :global LogPrintOnce;
:global MAX;
:set FileName [ $CleanFilePath $FileName ];
@@ -1558,7 +1696,20 @@
: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={
+ $LogPrintOnce warning $0 \
+ ("Hit the infamous file handling breakage (SUP-179200) introduced with RouterOS 7.18beta2...");
+ }
+ :delay $Delay;
+ :set Delay ($Delay * 3 / 2);
+ }
+
+ :return false;
}
# wait to be fully connected (default route is reachable, time is sync, DNS resolves)