]> Git [git.vados.ru] :: Repos - Mikrotiks/ros.vados.ru.git/commitdiff
Auto
authorvados-dev <192440777+vados-dev@users.noreply.github.com>
Wed, 1 Apr 2026 00:45:48 +0000 (03:45 +0300)
committervados-dev <192440777+vados-dev@users.noreply.github.com>
Wed, 1 Apr 2026 00:45:48 +0000 (03:45 +0300)
16 files changed:
html/AM-CheckUpdates.rsc
html/AM-GlobalFunc.rsc
html/AM-Init-Script.rsc
html/AM-SFTP-backup.rsc
html/AM-setup.rsc
html/README.md
html/global-functions.rsc [new file with mode: 0644]
html/logo/index.html
html/mod/AM-DateTime.rsc
html/mod/AM-GitBackup.rsc [moved from html/AM-GitBackup.rsc with 89% similarity]
html/mod/AM-IPbridge.rsc [new file with mode: 0644]
html/mod/AM-IPcalc.rsc [moved from html/mod/ipcalc.rsc with 53% similarity]
html/mod/AM-RunOnce.rsc
html/mod/AM-TG-Bot.rsc
html/mod/AM-TG-Notifications.rsc
html/mod/AM-UpdateCertsCustom.rsc [moved from html/Update-Certificates.rsc with 95% similarity]

index d137fe875b3e08338a8035a3d5070efd5e559a08..9fbbaefd74150484ee75983d41d46113cfb983bf 100644 (file)
@@ -41,7 +41,7 @@
   :set ExitOK true;
   :error false;
 }
-  
+
   :local DoUpdate do={
     :local ScriptName [ :tostr $1 ];
 
index 3600c586991f1021a682d7ab631bf1d8c48606c4..7e27cdf6fe8ea158a73446b902d823b8c0fe3dcb 100644 (file)
@@ -16,9 +16,9 @@
 :global GlobalFuncReady false;
 
 # Git commit id & info, expected configuration version
-:global CommitId "main/196/3419e47d";
+:global CommitId "main/197/6f15f19c";
 :global CommitTAG "unknown";
-:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 04:35:03 18.03.2026.";
+:global CommitInfo "Autocommit in repo: ros.vados.ru, generated by owner: root, in 03:45:02 01.04.2026.";
 
 # The variables are received right here.
 :global GlobalConfVersion 138;
     :retry command={/system/package/update set channel=$updateChannel}  delay=300ms max=5 on-error={};
     $LogPrint debug $0 ("Set updates channel to: $updateChannel");
   }
-   
+
   :local Update [ /system/package/update/get ];
   :local Channel ($Update->"channel");
   $LogPrint debug $0 ("Update Channel: " . $Channel . ".");
-   
+
   :local StatusPkg "undefined";
   $LogPrint debug $0 ("Checking for updates...");
   :retry command={:set $StatusPkg ([/system/package/update/check-for-updates without-paging as-value]->"status")} \
 
    :global devBoardName;
    :global devModel;
-   
+
    :global arrDeviceInfo [:toarray {"section"={"key"=""; "value"="";} }];
    #:do {:set ($arrDeviceInfo->"Updates") [[ :parse "/system/package/update/get" ]]} on-error={}
    :global devRbRevision;
    :global devRbSerialNumber;
    :global devRbCurrentFw;
    :global devRbUpgradeFw;
- 
+
    :global devRunning;
    :global devCurArch;
    :global ROsVerCurrent;
    :global runningVersion;
    :global devBuildTime;
    :global GetPubIp;
-   
+
    :local License [/system/license/get];
    :local Resource [/system/resource/get];
    :set devBuildTime ($Resource->"build-time");
    :set runningChannelVersion ($Resource->"version");
    :set runningChannel [$ParseCustom $runningChannelVersion "(" ")" 1];
    :set runningVersion [$ParseCustom $runningChannelVersion " "];
-   
+
    :do {
    :if ([:pick $devBoardName 0 3] != "CHR" or [:pick $devBoardName 0 3] != "x86") do={
       :local RouterBoard;
   :local Resource [/system/resource/get];
   :local Snmp [ /snmp/get ];
   :local Update [ /system/package/update/get ];
-  
+
   :set Return ([ $FormatLine "Hostname:" $Identity ] . "\n" . \
     [ $IfThenElse ([ :len ($Snmp->"location") ] > 0) \
       ([ $FormatLine "Location:" ($Snmp->"location") ] . "\n") ] . \
index 64ff49f61e072df8cd69db46614f2b4368921ce4..42b7d2c9759aca48eb1cac2aee1272ea634518b1 100644 (file)
@@ -1,98 +1,98 @@
-#!rsc by Vados\r
-# RouterOS script: AM-Init\r
-# Script comment: Initial Script for setup routeros scrypt system\r
-# Copyright (c) 2005-2026 Vados <vados@vados.ru>\r
-#\r
-#\r
-# requires RouterOS, version=7.19\r
-# requires device-mode, fetch\r
-#\r
-# Additional commands:\r
-# /system/script/set source=[ :tocrlf [ get $ScriptName source ] ] $ScriptName;\r
-#\r
-#:global GlobalEnvRemove true; /system/script {run Amster-GlobalEnvRemove; };\r
-#:delay 1s;\r
-#:global ReloadGlobal true; \r
-\r
-:global GlobalScriptsUrl "https://ros.vados.ru/";\r
-:global defOwner "vados";\r
-:global InitFirstRun;\r
-\r
-:set InitFirstRun do={\r
-    :global GlobalScriptsUrl;\r
-    :global defOwner;\r
-    #:global ParseKeyValueStore;\r
-    #:local AmUpdCRLF true;\r
-    #:local IfThenElse;\r
-    #:set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={:return $2}; :return $3}\r
-    #/certificate/settings/set builtin-trust-store=fetch;\r
-    #/tool/fetch "$scriptsUrl/certs/Root-YE.pem" dst-path="root-ye.pem";\r
-    #/certificate/import file-name="root-ye.pem" passphrase="";\r
-    # For basic verification we rename the certificate and print it by fingerprint. Make sure exactly this one certificate ("Root-YE") is shown.\r
-    #/certificate/set name="Root-YE" [ find where common-name="Root YE" ];\r
-    #/certificate/print proplist=name,fingerprint where fingerprint="e14ffcad5b0025731006caa43a121a22d8e9700f4fb9cf852f02a708aa5d5666";\r
-    #:log info "\nGlobalScriptsUrl = $GlobalScriptsUrl";\r
-    :local Scripts {\r
-        "Amster-GlobalConfig"\r
-        "Amster-GlobalFunc";\r
-#        "Amster-SFTP-backup"\r
-#        "Amster-BkpEmail-and-upd";\r
-    };\r
-#    /system/script/set owner=($ScriptVal->"name") \\r
-#    source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script;\r
-    :foreach Script in=$Scripts do={\r
-        :put "Installing $Script...";\r
-        /system/script/remove [ find where name=$Script ];\r
-        /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data");\r
-        /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script;\r
-    };\r
-    /system/script { run Amster-GlobalConfig; run Amster-GlobalFunc; }\r
-}\r
-:global InitUpdRun;\r
-:set InitUpdRun do={\r
-    #:local UpdScript [ :tostr $1 ];\r
-    #:global ParseKeyValueStore;\r
-    :global GlobalScriptsUrl;\r
-    :global defOwner;\r
-    :local Scripts {\r
-#        "Amster-GlobalConfig"\r
-#        "Amster-GlobalFunc"\r
-#        "Amster-GlobalRemove"\r
-        "Amster-backup-SFTP"\r
-        "Amster-SFTP-backup"\r
-        "Amster-BkpEmail-and-upd"\r
-        "Amster-TG-Bot"\r
-        "Amster-TG-Notification";\r
-        };\r
-    :foreach Script in=$Scripts do={\r
-        :put "updating $Script...";\r
-        /system/script/remove [ find where name=$Script ];\r
-        /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data");\r
-        /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script;\r
-    };\r
-#    /system/script/add name=$Script owner=$AmOwner source=([ /tool/fetch check-certificate=yes-without-crl ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); };\r
-}\r
-\r
-$InitFirstRun;\r
-#$InitUpdRun;\r
-\r
-#$setUpdExist;\r
-#:global setUpdExist;\r
-#:global ParseKeyValueStore;\r
-#:set setUpdExist do={\r
-#:do {\r
-#:global ParseKeyValueStore;\r
-#  :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados\r?\n#comment=*" ] do={\r
-#    :local ScriptVal [ /system/script/get $Script ];\r
-#    :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"#comment") ];\r
-#    :local scriptName ($ScriptVal->"name");\r
-#:log warning ("\n\$scriptName = ". $scriptName);\r
-#: log warning ("\n\$ScriptInfo = ". $ScriptInfo);\r
-#  }\r
-#} on-error={}\r
-#}\r
-#$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;\r
-#;(evl / (evl /docommand=;(evl / (evl /foreachcounter=$Script;do=;(evl / (evl /localname=$ScriptVal;value=(evl (evl /system/script/getnumber=$Script))) (evl /localname=$ScriptInfo;value=(evl (<%% $ParseKeyValueStore (> $ParseKeyValueStore);(-> $ScriptVal comment)))) (evl /localname=$scriptName) (evl /setname=$scriptName;value=(-> #$ScriptVal name)) (evl /log/errormessage=(. \r
-# $ ScriptInfo =  $ScriptInfo)));in=(evl (evl /system/script/findwhere=$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;(~ $source (. ^#!rsc by Vados \r
-# ? \r
-#));5))));on-error=;(evl /)))\r
+#!rsc by Vados
+# RouterOS script: AM-Init
+# Script comment: Initial Script for setup routeros scrypt system
+# Copyright (c) 2005-2026 Vados <vados@vados.ru>
+#
+#
+# requires RouterOS, version=7.19
+# requires device-mode, fetch
+#
+# Additional commands:
+# /system/script/set source=[ :tocrlf [ get $ScriptName source ] ] $ScriptName;
+#
+#:global GlobalEnvRemove true; /system/script {run Amster-GlobalEnvRemove; };
+#:delay 1s;
+#:global ReloadGlobal true; 
+
+:global GlobalScriptsUrl "https://ros.vados.ru/";
+:global defOwner "vados";
+:global InitFirstRun;
+
+:set InitFirstRun do={
+    :global GlobalScriptsUrl;
+    :global defOwner;
+    #:global ParseKeyValueStore;
+    #:local AmUpdCRLF true;
+    #:local IfThenElse;
+    #:set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={:return $2}; :return $3}
+    #/certificate/settings/set builtin-trust-store=fetch;
+    #/tool/fetch "$scriptsUrl/certs/Root-YE.pem" dst-path="root-ye.pem";
+    #/certificate/import file-name="root-ye.pem" passphrase="";
+    # For basic verification we rename the certificate and print it by fingerprint. Make sure exactly this one certificate ("Root-YE") is shown.
+    #/certificate/set name="Root-YE" [ find where common-name="Root YE" ];
+    #/certificate/print proplist=name,fingerprint where fingerprint="e14ffcad5b0025731006caa43a121a22d8e9700f4fb9cf852f02a708aa5d5666";
+    #:log info "\nGlobalScriptsUrl = $GlobalScriptsUrl";
+    :local Scripts {
+        "Amster-GlobalConfig"
+        "Amster-GlobalFunc";
+#        "Amster-SFTP-backup"
+#        "Amster-BkpEmail-and-upd";
+    };
+#    /system/script/set owner=($ScriptVal->"name") \
+#    source=[ $IfThenElse ($ScriptUpdatesCRLF = true) $SourceCRLF $SourceNew ] $Script;
+    :foreach Script in=$Scripts do={
+        :put "Installing $Script...";
+        /system/script/remove [ find where name=$Script ];
+        /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data");
+        /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script;
+    };
+    /system/script { run Amster-GlobalConfig; run Amster-GlobalFunc; }
+}
+:global InitUpdRun;
+:set InitUpdRun do={
+    #:local UpdScript [ :tostr $1 ];
+    #:global ParseKeyValueStore;
+    :global GlobalScriptsUrl;
+    :global defOwner;
+    :local Scripts {
+#        "Amster-GlobalConfig"
+#        "Amster-GlobalFunc"
+#        "Amster-GlobalRemove"
+        "Amster-backup-SFTP"
+        "Amster-SFTP-backup"
+        "Amster-BkpEmail-and-upd"
+        "Amster-TG-Bot"
+        "Amster-TG-Notification";
+        };
+    :foreach Script in=$Scripts do={
+        :put "updating $Script...";
+        /system/script/remove [ find where name=$Script ];
+        /system/script/add name=$Script owner=$defOwner source=([ /tool/fetch ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data");
+        /system/script/set source=[ :tocrlf [ get $Script source ] ] $Script;
+    };
+#    /system/script/add name=$Script owner=$AmOwner source=([ /tool/fetch check-certificate=yes-without-crl ("$GlobalScriptsUrl" . $Script . ".rsc") output=user as-value ]->"data"); };
+}
+
+$InitFirstRun;
+#$InitUpdRun;
+
+#$setUpdExist;
+#:global setUpdExist;
+#:global ParseKeyValueStore;
+#:set setUpdExist do={
+#:do {
+#:global ParseKeyValueStore;
+#  :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados\r?\n#comment=*" ] do={
+#    :local ScriptVal [ /system/script/get $Script ];
+#    :local ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"#comment") ];
+#    :local scriptName ($ScriptVal->"name");
+#:log warning ("\n\$scriptName = ". $scriptName);
+#: log warning ("\n\$ScriptInfo = ". $ScriptInfo);
+#  }
+#} on-error={}
+#}
+#$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;
+#;(evl / (evl /docommand=;(evl / (evl /foreachcounter=$Script;do=;(evl / (evl /localname=$ScriptVal;value=(evl (evl /system/script/getnumber=$Script))) (evl /localname=$ScriptInfo;value=(evl (<%% $ParseKeyValueStore (> $ParseKeyValueStore);(-> $ScriptVal comment)))) (evl /localname=$scriptName) (evl /setname=$scriptName;value=(-> #$ScriptVal name)) (evl /log/errormessage=(. 
+# $ ScriptInfo =  $ScriptInfo)));in=(evl (evl /system/script/findwhere=$name;$owner;$policy;$dont-require-permissions;$last-started;$run-count;$source;$invalid;$comment;$.id;$.nextid;$.dead;$.about;(~ $source (. ^#!rsc by Vados 
+# ? 
+#));5))));on-error=;(evl /)))
index ec8d6d2ba6ef7bfa9f6273c147138b237963d686..868d2c8035e7e499d982c3097476fe8b67d2b80a 100644 (file)
-#!rsc by Vados\r
-# RouterOS script: AM-SFTP-backup\r
-# Script comment: BackUp via /tool fetch SFTP\r
-# Copyright (c) 2005-2026 Vados <vados@vados.ru>\r
-#\r
-#\r
-#\r
-# requires RouterOS, version=7.21\r
-# requires device-mode, fetch, scheduler\r
-#\r
-# Based on:\r
-# https://forum.mikrotik.com/viewtopic.php?t=159432\r
-# https://forum.mikrotik.com/viewtopic.php?p=858564#p858564\r
-#\r
-### Info Log Action\r
-# $stage (string) selects action text\r
-# $msg (string) additional message, usually the backup stage or filename\r
-# $error (bool) (optional) creates error log instead of info if 'true'\r
-:local ExitOK false;\r
-:onerror Err {\r
-  :global GlobalConfReady; :global GlobalFuncReady;\r
-  :retry { :if ($GlobalConfReady != true || $GlobalFuncReady != true) \\r
-      do={ :error ("Global configs or functions not ready."); }; } delay=500ms max=50;\r
-  :local ScriptName [ :jobname ];\r
-\r
-:global Identity;\r
-:global checkRunJob;\r
-:global LogPrint;\r
-:global RandomDelay;\r
-:global MkDir;\r
-:global RmDir;\r
-:global RmFile;\r
-:global FileGet;\r
-:global WaitForFile;\r
-:global MiniDateTimeStamp;\r
-:global OSVersion;\r
-:global devBoardName;\r
-:global BackupRandomDelay;\r
-:global BackupUploadUrl;\r
-:global BackupUploadUser;\r
-:global BackupUploadPass;\r
-:global BackupRmLocal;\r
-:global BackupSendBinary;\r
-:global BackupLocalRoot;\r
-:global BackupEncrypt;\r
-:global BackupPassword;\r
-:global BackupSens;\r
-:global BackupSendGlobalConfig;\r
-:global BackupUser;\r
-:global BackupLicense;\r
-:global BackupSshKeys;\r
-:global BackupCerts;\r
-:global BackupCertsPasswd;\r
-:global BackupUserMan;\r
-:global BackupDude;\r
-:global BackupUserList;\r
-:global SendTelegram;\r
-:global SendNotification;\r
-### End Configuration\r
-  \r
-  :if ([ :len [ /system/scheduler/find where name="BackupEveryDaySFTP" ] ] = 0) do={\r
-    $LogPrint warning $ScriptName ("SystemScheduler NOT SET!");\r
-    /system/scheduler/add name=$ScriptName on-event="/system/script { run $ScriptName; }" comment="Backup Every Day on SFTP" interval=1w policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon; start-time=startup; \r
-  }\r
-\r
-  :if ([ $checkRunJob $ScriptName ] = false && $BackupRandomDelay > 0) do={\r
-    $RandomDelay $BackupRandomDelay;\r
-  }\r
-\r
-:local dateTimeStampFix [$MiniDateTimeStamp];\r
-:local bkpDirName ($Identity . "-" . $dateTimeStampFix);\r
-:local BkpPath;\r
-:local FilePref ($Identity . "-");\r
-\r
-:if ($BackupLocalRoot = "") do={\r
-    $LogPrint warning $ScriptName ("\n\$BackupLocalRoot is not set! I set \$BackupRmLocal to true!" . $Err);\r
-    :set $BackupRmLocal true;\r
-:do {\r
-$MkDir ($bkpDirName);\r
-:set BkpPath $bkpDirName;\r
-   } on-error={$LogPrint error $ScriptName ("ERROR Create Directory $BkpPath" . $Err);}\r
-} else={\r
-:do {\r
-$MkDir ($BackupLocalRoot . "/" . $bkpDirName);\r
-:set BkpPath ($BackupLocalRoot . "/" . $bkpDirName);\r
-   } on-error={$LogPrint error $ScriptName ("ERROR Create Directories $BkpPath" . $Err);}\r
-}\r
-:local locFilePref ($BkpPath . "/" . $FilePref);\r
-\r
-$LogPrint warning $ScriptName ("\$locFilePref = $locFilePref" . $Err);\r
-\r
-:local remFilePref ($FilePref . $dateTimeStampFix . "-");\r
-#($bkpDirName . "/" . $BkpFilePrefix);\r
-#:if ($SFTPpath != "") do={\r
-#  :set remFilePref ($SFTPpath . "/" . $remFilePref);\r
-#} else={:set remFilePref ("/" . $remFilePref)}\r
-\r
-$LogPrint warning $ScriptName ("\$remFilePref = $remFilePref");\r
-\r
-### Process local filename to create remote filename\r
-### Return array for file array\r
-## Strips path separator '/' from local file name and replaces with '_'\r
-# 'lfile' (string) the local filename\r
-# 'lpref' (string) the local prefix to strip from start of lfile (if present)\r
-# 'rpref' (string) remote prefix to prepend to remote filename\r
-# 'clear' (bool) whether to delete local file after uploading\r
-:local dofnames do={\r
-  :local rfile "";\r
-  :local rfilef "";\r
-  # Strip Local Prefix if present\r
-  if ([:find $lfile $lpref -1] = 0) do={\r
-    :set rfile [:pick $lfile [:len $lpref] [:len $lfile]];\r
-  } else={\r
-    :set rfile $lfile;\r
-  }\r
-  $LogPrint warning $ScriptName ("\n\$rfile = $rfile");\r
-  # Convert / to _\r
-  :for i from=0 to=([:len $rfile] - 1) do={\r
-    :local char [:pick $rfile $i];\r
-    :if ($char = "/") do={\r
-      :set $char "_";\r
-    }\r
-    :set rfilef ($rfilef . $char);\r
-    $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef);\r
-    $LogPrint warning $ScriptName ("\n\$char = " . $char);\r
-  }\r
-  # Prepend Remote Prefix\r
-  :set rfile ($rpref . $rfilef);\r
-  $LogPrint warning $ScriptName ("\n\$rfile = " . $rfile);\r
-  $LogPrint warning $ScriptName ("\n\$rpref = " . $rpref);\r
-  $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef);\r
-  $LogPrint warning $ScriptName ("\n\$lfile = " . $lfile);\r
-  $LogPrint warning $ScriptName ("\n\$clear = " . $clear);\r
-  # Return array\r
-  :return {lfile=$lfile; rfile=$rfile; clear=$clear};\r
-}\r
-\r
-### Delete Local File(s)\r
-# $lfile (string) local file to be deleted\r
-:local dodelete do={\r
-  :global BackupRmLocal;\r
-  :global RmFile;\r
-  :global LogPrint;\r
-  if ($BackupRmLocal = true) do={\r
-    :do {\r
-      $RmFile $lfile;\r
-    } on-error={\r
-      $LogPrint error $ScriptName ("Error remove file " . $lfile . "." . $Err);\r
-    }\r
-#  if ([:len [/file find where name="$lfile"]] > 0) do={\r
-#    /file remove [find where name="$lfile"];\r
-  }\r
-}\r
-\r
-:local filesa [:toarray ""];\r
-:local cfilename "";\r
-:local lfilename "";\r
-:local rfilename "";\r
-\r
-### Binary Backup\r
-if ($BackupSendBinary = true) do={\r
-  :set cfilename ($locFilePref . "backup");\r
-  :set lfilename ($cfilename . ".backup");\r
-  if ($BackupEncrypt = false) do={\r
-    :do {\r
-      /system backup save dont-encrypt=yes name=$cfilename;\r
-    } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " dont-encrypted failed" . $Err);}\r
-  } else={\r
-    :do {\r
-      /system backup save encryption=aes-sha256 name=$cfilename password=$BackupPassword;\r
-    } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " encrypted failed" . $Err);}\r
-  }\r
-  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-}\r
-\r
-### Generic Export\r
-if ($BackupSendGlobalConfig = true) do={\r
-  :set cfilename ($locFilePref . "export");\r
-  :set lfilename ($cfilename . ".rsc");\r
-  if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={\r
-    :do {\r
-      /export compact file=$cfilename;\r
-    } on-error={$LogPrint error $ScriptName ("Export Global compact " .$cfilename . " failed" . $Err);}\r
-  } else={\r
-    if ($OSVersion = "6") do={\r
-      $LogPrint info $ScriptName ("Start Global compact if OSVersion = 6 (hide-sensitive)");\r
-      :do {\r
-        /export compact hide-sensitive file=$cfilename;\r
-      } on-error={$LogPrint error $ScriptName ("Export Global compact if OSVersion = 6 (hide-sensitive) " .$cfilename . " failed" . $Err);}\r
-    } else={\r
-      $LogPrint info $ScriptName ("Start Global compact");\r
-      :do {\r
-        if ($BackupSens = true) do={\r
-        /export compact hide-sensitive file=$cfilename;\r
-        } else={\r
-          /export compact show-sensitive file=$cfilename;\r
-        }\r
-      } on-error={$LogPrint error $ScriptName ("Export Global compact (\$BackupSens = " . $BackupSens . ") " .$cfilename . " failed" . $Err);}\r
-    }\r
-  }\r
-  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-}\r
-\r
-### User Export\r
-if ($BackupUser = true) do={\r
-  :set cfilename ($locFilePref . "user");\r
-  :set lfilename ($cfilename . ".rsc");\r
-  if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={\r
-    :do {\r
-      /user export compact file=$cfilename;\r
-      } on-error={$LogPrint error $ScriptName ("User Export " . $cfilename . " failed" . $Err);}\r
-  } else={\r
-    if ($OSVersion = "6") do={\r
-      $LogPrint info $ScriptName ("User Export (hide-sensitive)" . $lfilename);\r
-      :do {\r
-        /user export compact hide-sensitive file=$cfilename;\r
-      } on-error={$LogPrint error $ScriptName ("User Export (hide-sensitive) " .$cfilename . " failed" . $Err);}\r
-    } else={\r
-      $LogPrint info $ScriptName ("User Export (show-sensitive) " . $lfilename);\r
-      :do {\r
-        /user export compact show-sensitive file=$cfilename;\r
-      } on-error={$LogPrint error $ScriptName ("User Export (show-sensitive) " .$cfilename . " failed" . $Err);}\r
-    }\r
-  }\r
-  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-}\r
-\r
-### License Export\r
-if ($BackupLicense = true and $devBoardName != "CHR") do={\r
-  :set lfilename ([/system license get software-id] . ".key");\r
-  :set rfilename ($remFilePref . "license.key");\r
-  :do {\r
-    /system license output;\r
-  } on-error={$LogPrint error $ScriptName ("Backup " . $lfilename . " failed" . $Err);}\r
-  :set ($filesa->([:len $filesa])) {lfile=$lfilename; rfile=$rfilename; clear=$BackupRmLocal};\r
-}\r
-\r
-### SSH Keys\r
-if ($BackupSshKeys = true) do={\r
-  :set cfilename ($locFilePref . "host-key");\r
-  :do {\r
-    /ip ssh export-host-key key-file-prefix=$cfilename;\r
-  } on-error={$LogPrint error $ScriptName ("Backup SSH Keys " . $cfilename . " failed" . $Err);}\r
-  :foreach lfile in=[/file find where name~"^$cfilename"] do={\r
-    :set ($filesa->([:len $filesa])) [$dofnames lfile=[/file get $lfile name] lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-  }\r
-}\r
-\r
-### Certificates\r
-if ($BackupCerts = true) do={\r
-  :foreach cert in=[/certificate find] do={\r
-    :local certname [/certificate get $cert name];\r
-    :local cfilename ($locFilePref . "cert-" . $certname);\r
-    :do {\r
-      /certificate export-certificate $cert file-name=$cfilename \\r
-                                      type=pkcs12 export-passphrase=$BackupCertsPasswd;\r
-    } on-error={$LogPrint error $ScriptName ("Backup Certificate " . $cfilename . " failed" . $Err);}\r
-    :set ($filesa->([:len $filesa])) [$dofnames lfile=($cfilename . ".p12") lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-  }\r
-}\r
-\r
-# User-Manager\r
-if ($BackupUserMan = true) do={\r
-  :set cfilename ($locFilePref . "user-manager");\r
-  :set lfilename ($cfilename . ".umb");\r
-  :do {\r
-    $dodelete lfile=$lfilename;\r
-  } on-error={$LogPrint error $ScriptName ("User-Manager " . $lfilename . " clear failed" . $Err);}\r
-  if ($OSVersion = "6") do={\r
-    :do {\r
-      /tool user-manager database save name=$cfilename;\r
-    } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);}\r
-  }\r
-  if ($OSVersion = "7") do={\r
-    :do {\r
-      /user-manager database save name=$cfilename;\r
-    } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);}\r
-  }\r
-  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-}\r
-\r
-# The Dude\r
-if ($BackupDude = true) do={\r
-  :set lfilename ($locFilePref . "the-dude.db");\r
-  :do {\r
-    $dodelete lfile=$lfilename;\r
-  } on-error={$LogPrint error $ScriptName ("Dude " . $lfilename . " clear failed" . $Err);}\r
-  $dolog stage="create" msg=$logstage;\r
-  :do {\r
-    /dude export-db backup-file=$lfilename;\r
-  } on-error={$LogPrint error $ScriptName ("Backup Dude " . $lfilename . " failed" . $Err);}\r
-  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];\r
-}\r
-\r
-# User File List\r
-if ([:len $BackupUserList] > 0) do={\r
-  :foreach lfile in=[:toarray $BackupUserList] do={\r
-    :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfile lpref=$locFilePref rpref=$remFilePref clear=false];\r
-  }\r
-}\r
-\r
-# Process Files Array\r
-:local lfile "";\r
-:local rfile "";\r
-:local clear true;\r
-/delay 10s;\r
-:foreach a in=$filesa do={\r
-  :set lfile ($a->"lfile");\r
-  :set rfile ($a->"rfile");\r
-  :set clear ($a->"clear");\r
-  if ([:len [/file find where name="$lfile"]] > 0) do={\r
-    $LogPrint info $ScriptName ("Upload " . $lfile . " AS " . $rfile);\r
-    :do {\r
-      /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $rfile) \\r
-          user=$BackupUploadUser password=$BackupUploadPass src-path=$lfile;\r
-#      /tool fetch address=$SFTPsrv user=$SFTPusr password=$SFTPpasswd src-path=$lfile dst-path=$rfile upload=yes mode=sftp;\r
-    } on-error={$LogPrint error $ScriptName ($rfile . $Err);}\r
-    if ($clear = true) do={\r
-      $LogPrint info $ScriptName ("Delete " . $lfile)\r
-      :do {\r
-        :if ($BackupRmLocal = true) do={$RmFile $lfile}\r
-      } on-error={$LogPrint error $ScriptName ($lfile . $Err);}\r
-    }\r
-  }\r
-}\r
-:if (!$ExitError && $BackupRmLocal = true) do={$RmDir $BkpPath}\r
-} do={:global ExitError; $ExitError $ExitOK [ :jobname ] $Err}\r
+#!rsc by Vados
+# RouterOS script: AM-SFTP-backup
+# Script comment: BackUp via /tool fetch SFTP
+# Copyright (c) 2005-2026 Vados <vados@vados.ru>
+#
+#
+# requires RouterOS, version=7.21
+# requires device-mode, fetch, scheduler
+#
+# Based on:
+# https://forum.mikrotik.com/viewtopic.php?t=159432
+# https://forum.mikrotik.com/viewtopic.php?p=858564#p858564
+#
+### Info Log Action
+# $stage (string) selects action text
+# $msg (string) additional message, usually the backup stage or filename
+# $error (bool) (optional) creates error log instead of info if 'true'
+:local ExitOK false;
+:onerror Err {
+  :global GlobalConfReady; :global GlobalFuncReady;
+  :retry { :if ($GlobalConfReady != true || $GlobalFuncReady != true) \
+      do={ :error ("Global configs or functions not ready."); }; } delay=500ms max=50;
+  :local ScriptName [ :jobname ];
+
+:global Identity;
+:global checkRunJob;
+:global LogPrint;
+:global RandomDelay;
+:global MkDir;
+:global RmDir;
+:global RmFile;
+:global FileGet;
+:global WaitForFile;
+:global MiniDateTimeStamp;
+:global OSVersion;
+:global devBoardName;
+:global BackupRandomDelay;
+:global BackupUploadUrl;
+:global BackupUploadUser;
+:global BackupUploadPass;
+:global BackupRmLocal;
+:global BackupSendBinary;
+:global BackupLocalRoot;
+:global BackupEncrypt;
+:global BackupPassword;
+:global BackupSens;
+:global BackupSendGlobalConfig;
+:global BackupUser;
+:global BackupLicense;
+:global BackupSshKeys;
+:global BackupCerts;
+:global BackupCertsPasswd;
+:global BackupUserMan;
+:global BackupDude;
+:global BackupUserList;
+:global SendTelegram;
+:global SendNotification;
+### End Configuration
+
+  :if ([ :len [ /system/scheduler/find where name="BackupEveryDaySFTP" ] ] = 0) do={
+    $LogPrint warning $ScriptName ("SystemScheduler NOT SET!");
+    /system/scheduler/add name=$ScriptName on-event="/system/script { run $ScriptName; }" comment="Backup Every Day on SFTP" interval=1w policy=ftp,reboot,read,write,policy,test,password,sniff,sensitive,romon; start-time=startup; 
+  }
+
+  :if ([ $checkRunJob $ScriptName ] = false && $BackupRandomDelay > 0) do={
+    $RandomDelay $BackupRandomDelay;
+  }
+
+:local dateTimeStampFix [$MiniDateTimeStamp];
+:local bkpDirName ($Identity . "-" . $dateTimeStampFix);
+:local BkpPath;
+:local FilePref ($Identity . "-");
+
+:if ($BackupLocalRoot = "") do={
+    $LogPrint warning $ScriptName ("\n\$BackupLocalRoot is not set! I set \$BackupRmLocal to true!" . $Err);
+    :set $BackupRmLocal true;
+:do {
+$MkDir ($bkpDirName);
+:set BkpPath $bkpDirName;
+   } on-error={$LogPrint error $ScriptName ("ERROR Create Directory $BkpPath" . $Err);}
+} else={
+:do {
+$MkDir ($BackupLocalRoot . "/" . $bkpDirName);
+:set BkpPath ($BackupLocalRoot . "/" . $bkpDirName);
+   } on-error={$LogPrint error $ScriptName ("ERROR Create Directories $BkpPath" . $Err);}
+}
+:local locFilePref ($BkpPath . "/" . $FilePref);
+
+$LogPrint warning $ScriptName ("\$locFilePref = $locFilePref" . $Err);
+
+:local remFilePref ($FilePref . $dateTimeStampFix . "-");
+#($bkpDirName . "/" . $BkpFilePrefix);
+#:if ($SFTPpath != "") do={
+#  :set remFilePref ($SFTPpath . "/" . $remFilePref);
+#} else={:set remFilePref ("/" . $remFilePref)}
+
+$LogPrint warning $ScriptName ("\$remFilePref = $remFilePref");
+
+### Process local filename to create remote filename
+### Return array for file array
+## Strips path separator '/' from local file name and replaces with '_'
+# 'lfile' (string) the local filename
+# 'lpref' (string) the local prefix to strip from start of lfile (if present)
+# 'rpref' (string) remote prefix to prepend to remote filename
+# 'clear' (bool) whether to delete local file after uploading
+:local dofnames do={
+  :local rfile "";
+  :local rfilef "";
+  # Strip Local Prefix if present
+  if ([:find $lfile $lpref -1] = 0) do={
+    :set rfile [:pick $lfile [:len $lpref] [:len $lfile]];
+  } else={
+    :set rfile $lfile;
+  }
+  $LogPrint warning $ScriptName ("\n\$rfile = $rfile");
+  # Convert / to _
+  :for i from=0 to=([:len $rfile] - 1) do={
+    :local char [:pick $rfile $i];
+    :if ($char = "/") do={
+      :set $char "_";
+    }
+    :set rfilef ($rfilef . $char);
+    $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef);
+    $LogPrint warning $ScriptName ("\n\$char = " . $char);
+  }
+  # Prepend Remote Prefix
+  :set rfile ($rpref . $rfilef);
+  $LogPrint warning $ScriptName ("\n\$rfile = " . $rfile);
+  $LogPrint warning $ScriptName ("\n\$rpref = " . $rpref);
+  $LogPrint warning $ScriptName ("\n\$rfilef = " . $rfilef);
+  $LogPrint warning $ScriptName ("\n\$lfile = " . $lfile);
+  $LogPrint warning $ScriptName ("\n\$clear = " . $clear);
+  # Return array
+  :return {lfile=$lfile; rfile=$rfile; clear=$clear};
+}
+
+### Delete Local File(s)
+# $lfile (string) local file to be deleted
+:local dodelete do={
+  :global BackupRmLocal;
+  :global RmFile;
+  :global LogPrint;
+  if ($BackupRmLocal = true) do={
+    :do {
+      $RmFile $lfile;
+    } on-error={
+      $LogPrint error $ScriptName ("Error remove file " . $lfile . "." . $Err);
+    }
+#  if ([:len [/file find where name="$lfile"]] > 0) do={
+#    /file remove [find where name="$lfile"];
+  }
+}
+
+:local filesa [:toarray ""];
+:local cfilename "";
+:local lfilename "";
+:local rfilename "";
+
+### Binary Backup
+if ($BackupSendBinary = true) do={
+  :set cfilename ($locFilePref . "backup");
+  :set lfilename ($cfilename . ".backup");
+  if ($BackupEncrypt = false) do={
+    :do {
+      /system backup save dont-encrypt=yes name=$cfilename;
+    } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " dont-encrypted failed" . $Err);}
+  } else={
+    :do {
+      /system backup save encryption=aes-sha256 name=$cfilename password=$BackupPassword;
+    } on-error={$LogPrint error $ScriptName ("Binary backup save " .$cfilename . " encrypted failed" . $Err);}
+  }
+  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+}
+
+### Generic Export
+if ($BackupSendGlobalConfig = true) do={
+  :set cfilename ($locFilePref . "export");
+  :set lfilename ($cfilename . ".rsc");
+  if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={
+    :do {
+      /export compact file=$cfilename;
+    } on-error={$LogPrint error $ScriptName ("Export Global compact " .$cfilename . " failed" . $Err);}
+  } else={
+    if ($OSVersion = "6") do={
+      $LogPrint info $ScriptName ("Start Global compact if OSVersion = 6 (hide-sensitive)");
+      :do {
+        /export compact hide-sensitive file=$cfilename;
+      } on-error={$LogPrint error $ScriptName ("Export Global compact if OSVersion = 6 (hide-sensitive) " .$cfilename . " failed" . $Err);}
+    } else={
+      $LogPrint info $ScriptName ("Start Global compact");
+      :do {
+        if ($BackupSens = true) do={
+        /export compact hide-sensitive file=$cfilename;
+        } else={
+          /export compact show-sensitive file=$cfilename;
+        }
+      } on-error={$LogPrint error $ScriptName ("Export Global compact (\$BackupSens = " . $BackupSens . ") " .$cfilename . " failed" . $Err);}
+    }
+  }
+  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+}
+
+### User Export
+if ($BackupUser = true) do={
+  :set cfilename ($locFilePref . "user");
+  :set lfilename ($cfilename . ".rsc");
+  if (($OSVersion = "6" and $BackupSens = true) or ($OSVersion = "7" and $BackupSens = false)) do={
+    :do {
+      /user export compact file=$cfilename;
+      } on-error={$LogPrint error $ScriptName ("User Export " . $cfilename . " failed" . $Err);}
+  } else={
+    if ($OSVersion = "6") do={
+      $LogPrint info $ScriptName ("User Export (hide-sensitive)" . $lfilename);
+      :do {
+        /user export compact hide-sensitive file=$cfilename;
+      } on-error={$LogPrint error $ScriptName ("User Export (hide-sensitive) " .$cfilename . " failed" . $Err);}
+    } else={
+      $LogPrint info $ScriptName ("User Export (show-sensitive) " . $lfilename);
+      :do {
+        /user export compact show-sensitive file=$cfilename;
+      } on-error={$LogPrint error $ScriptName ("User Export (show-sensitive) " .$cfilename . " failed" . $Err);}
+    }
+  }
+  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+}
+
+### License Export
+if ($BackupLicense = true and $devBoardName != "CHR") do={
+  :set lfilename ([/system license get software-id] . ".key");
+  :set rfilename ($remFilePref . "license.key");
+  :do {
+    /system license output;
+  } on-error={$LogPrint error $ScriptName ("Backup " . $lfilename . " failed" . $Err);}
+  :set ($filesa->([:len $filesa])) {lfile=$lfilename; rfile=$rfilename; clear=$BackupRmLocal};
+}
+
+### SSH Keys
+if ($BackupSshKeys = true) do={
+  :set cfilename ($locFilePref . "host-key");
+  :do {
+    /ip ssh export-host-key key-file-prefix=$cfilename;
+  } on-error={$LogPrint error $ScriptName ("Backup SSH Keys " . $cfilename . " failed" . $Err);}
+  :foreach lfile in=[/file find where name~"^$cfilename"] do={
+    :set ($filesa->([:len $filesa])) [$dofnames lfile=[/file get $lfile name] lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+  }
+}
+
+### Certificates
+if ($BackupCerts = true) do={
+  :foreach cert in=[/certificate find] do={
+    :local certname [/certificate get $cert name];
+    :local cfilename ($locFilePref . "cert-" . $certname);
+    :do {
+      /certificate export-certificate $cert file-name=$cfilename \
+                                      type=pkcs12 export-passphrase=$BackupCertsPasswd;
+    } on-error={$LogPrint error $ScriptName ("Backup Certificate " . $cfilename . " failed" . $Err);}
+    :set ($filesa->([:len $filesa])) [$dofnames lfile=($cfilename . ".p12") lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+  }
+}
+
+# User-Manager
+if ($BackupUserMan = true) do={
+  :set cfilename ($locFilePref . "user-manager");
+  :set lfilename ($cfilename . ".umb");
+  :do {
+    $dodelete lfile=$lfilename;
+  } on-error={$LogPrint error $ScriptName ("User-Manager " . $lfilename . " clear failed" . $Err);}
+  if ($OSVersion = "6") do={
+    :do {
+      /tool user-manager database save name=$cfilename;
+    } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);}
+  }
+  if ($OSVersion = "7") do={
+    :do {
+      /user-manager database save name=$cfilename;
+    } on-error={$LogPrint error $ScriptName ("Backup User-Manager " . $cfilename . " failed" . $Err);}
+  }
+  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+}
+
+# The Dude
+if ($BackupDude = true) do={
+  :set lfilename ($locFilePref . "the-dude.db");
+  :do {
+    $dodelete lfile=$lfilename;
+  } on-error={$LogPrint error $ScriptName ("Dude " . $lfilename . " clear failed" . $Err);}
+  $dolog stage="create" msg=$logstage;
+  :do {
+    /dude export-db backup-file=$lfilename;
+  } on-error={$LogPrint error $ScriptName ("Backup Dude " . $lfilename . " failed" . $Err);}
+  :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfilename lpref=$locFilePref rpref=$remFilePref clear=$BackupRmLocal];
+}
+
+# User File List
+if ([:len $BackupUserList] > 0) do={
+  :foreach lfile in=[:toarray $BackupUserList] do={
+    :set ($filesa->([:len $filesa])) [$dofnames lfile=$lfile lpref=$locFilePref rpref=$remFilePref clear=false];
+  }
+}
+
+# Process Files Array
+:local lfile "";
+:local rfile "";
+:local clear true;
+/delay 10s;
+:foreach a in=$filesa do={
+  :set lfile ($a->"lfile");
+  :set rfile ($a->"rfile");
+  :set clear ($a->"clear");
+  if ([:len [/file find where name="$lfile"]] > 0) do={
+    $LogPrint info $ScriptName ("Upload " . $lfile . " AS " . $rfile);
+    :do {
+      /tool/fetch upload=yes url=($BackupUploadUrl . "/" . $rfile) \
+          user=$BackupUploadUser password=$BackupUploadPass src-path=$lfile;
+#      /tool fetch address=$SFTPsrv user=$SFTPusr password=$SFTPpasswd src-path=$lfile dst-path=$rfile upload=yes mode=sftp;
+    } on-error={$LogPrint error $ScriptName ($rfile . $Err);}
+    if ($clear = true) do={
+      $LogPrint info $ScriptName ("Delete " . $lfile)
+      :do {
+        :if ($BackupRmLocal = true) do={$RmFile $lfile}
+      } on-error={$LogPrint error $ScriptName ($lfile . $Err);}
+    }
+  }
+}
+:if (!$ExitError && $BackupRmLocal = true) do={$RmDir $BkpPath}
+} do={:global ExitError; $ExitError $ExitOK [ :jobname ] $Err}
index d72322abae46b5bc8c5268bac709ea76d2b4a994..db58eda74b04a8498db276cce86d952940383dbe 100644 (file)
@@ -20,7 +20,7 @@
 # For run from terminal:
 #/system/script/add name=AM-setup owner=AM-Setup source=([/tool/fetch check-certificate=yes-without-crl "https://ros.vados.ru/AM-setup.rsc" output=user as-value ]->"data");
 #
-:global ScriptUpdatesCRLF true;
+:global ScriptUpdatesCRLF "LF";
 if ($ScriptUpdatesCRLF != false) do={
 :foreach Script in=[ /system/script/find where source~"^#!rsc by Vados" ] do={
   if ($ScriptUpdatesCRLF = true) do={
index 5e6c9364d92ac4b1bb3ceb9148ee3c229357f450..4e797620328198f6757fa09330716520c4aac07b 100644 (file)
@@ -73,17 +73,14 @@ Huge configuration and lots of scripts give an extra risk. **Take care!**
     :global GlobalScriptsUrl "https://ros.vados.ru/";
 
     :global RemoveMe false;
-    
-    /system/script/add name="AM-setup" owner="A
-M-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc")  output=user as-value]->"data");
+
+    /system/script/add name="AM-setup" owner="AM-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc")  output=user as-value]->"data");
 
     /system/script/run AM-setup;
 
 * One string:
 
-    :global GlobalScriptsUrl "https://ros.vados.ru/"; :global RemoveMe false; /system/script/add name="AM-setup" owner="A
-M-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc")  output=user as-value]->"data");
- /system/script/run AM-setup;
+    :global GlobalScriptsUrl "https://ros.vados.ru/"; :global RemoveMe false; /system/script/add name="AM-setup" owner="AM-setup" source=([/tool/fetch check-certificate=yes-without-crl url=($GlobalScriptsUrl . "AM-setup.rsc")  output=user as-value]->"data"); /system/script/run AM-setup;
 
 
 > 💡️ **Hint**: To prevent the installation script from being deleted 30 seconds after its completion, you can use a global variable [`:global RemoveMe false;`]. Don't worry, the script will delete all its global variables. Shall we check? ;)
diff --git a/html/global-functions.rsc b/html/global-functions.rsc
new file mode 100644 (file)
index 0000000..cb74f99
--- /dev/null
@@ -0,0 +1,1958 @@
+#!rsc by RouterOS
+# RouterOS script: global-functions
+# Copyright (c) 2013-2026 Christian Hesse <mail@eworm.de>
+#                         Michael Gisbers <michael@gisbers.de>
+# https://rsc.eworm.de/COPYING.md
+#
+# requires RouterOS, version=7.19
+# requires device-mode, fetch, scheduler
+#
+# global functions
+# https://rsc.eworm.de/
+
+:local ScriptName [ :jobname ];
+
+# Git commit id & info, expected configuration version
+:global CommitId "unknown";
+:global CommitInfo "unknown";
+:global ExpectedConfigVersion 139;
+
+# global variables not to be changed by user
+:global GlobalFunctionsReady false;
+: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 CommitBrief;
+:global DeviceInfo;
+:global Dos2Unix;
+:global DownloadPackage;
+:global EitherOr;
+:global EscapeForRegEx;
+:global ExitError;
+:global ExitOnError;
+:global FetchHuge;
+:global FetchUserAgentStr;
+:global FileExists;
+:global FileGet;
+:global FormatLine;
+:global FormatMultiLines;
+:global GetMacVendor;
+:global GetRandom20CharAlNum;
+:global GetRandom20CharHex;
+:global GetRandomNumber;
+:global Grep;
+:global HexToNum;
+:global HumanReadableNum;
+:global IfThenElse;
+:global IsDefaultRouteReachable;
+:global IsDNSResolving;
+:global IsFullyConnected;
+:global IsMacLocallyAdministered;
+:global IsTimeSync;
+:global LogPrint;
+:global LogPrintOnce;
+:global LogPrintVerbose;
+:global MAX;
+:global MIN;
+:global MkDir;
+:global NetMask4;
+:global NetMask6;
+:global NotificationFunctions;
+:global ParseDate;
+:global ParseKeyValueStore;
+:global PrettyPrint;
+:global ProtocolStrip;
+:global RandomDelay;
+:global RequiredRouterOS;
+:global RmDir;
+:global RmFile;
+:global ScriptFromTerminal;
+:global ScriptInstallUpdate;
+:global ScriptLock;
+:global SendNotification;
+:global SendNotification2;
+:global SymbolByUnicodeName;
+:global SymbolForNotification;
+:global Unix2Dos;
+:global UrlEncode;
+:global ValidateSyntax;
+:global VersionToNum;
+:global WaitDefaultRouteReachable;
+:global WaitDNSResolving;
+:global WaitForFile;
+: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 ];
+  :local UseFor     [ :tostr $2 ];
+
+  :global CertificateDownload;
+  :global EitherOr;
+  :global LogPrint;
+  :global ParseKeyValueStore;
+
+  :set UseFor [ $EitherOr $UseFor "undefined" ];
+
+  :if ([ /system/resource/get free-hdd-space ] < 8388608 && \
+       [ /certificate/settings/get crl-download ] = true && \
+       [ /certificate/settings/get crl-store ] = "system") do={
+    $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;
+  }
+
+  :local CertSettings [ /certificate/settings/get ];
+  :if ((($CertSettings->"builtin-trust-anchors") = "trusted" || \
+        ($CertSettings->"builtin-trust-store") ~ $UseFor || \
+        ($CertSettings->"builtin-trust-store") = "all") && \
+       [ :len [ /certificate/builtin/find where common-name=$CommonName ] ] > 0) do={
+    :return true;
+  }
+
+  :if ([ :len [ /certificate/find where common-name=$CommonName ] ] = 0) do={
+    $LogPrint info $0 ("Certificate with CommonName '" . $CommonName . "' not available.");
+    :if ([ $CertificateDownload $CommonName ] = false) do={
+      :return false;
+    }
+  }
+
+  :if ([ :len [ /certificate/find where common-name=$CommonName ] ] > 1) do={
+    $LogPrint info $0 ("There are " . $CertCount . " Certificates with CommonName '" . $CommonName . "'. Should be ok.");
+    :return true;
+  }
+
+  :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={
+      :local IssuerCN ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN");
+      $LogPrint info $0 ("Certificate chain for '" . $CommonName . "' is incomplete, missing '" . $IssuerCN . "'.");
+      :if ([ $CertificateDownload $IssuerCN ] = 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 CleanName;
+  :global FetchUserAgentStr;
+  :global LogPrint;
+  :global RmFile;
+  :global WaitForFile;
+
+  $LogPrint info $0 ("Downloading and importing certificate with " . \
+      "CommonName '" . $CommonName . "'.");
+  :local FileName ([ $CleanName $CommonName ] . ".pem");
+  :do {
+    /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={
+    $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 Match [ :tostr $1 ];
+
+  :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;
+}
+
+# multiply given character(s)
+:set CharacterMultiply do={
+  :local Str [ :tostr $1 ];
+  :local Num [ :tonum $2 ];
+
+  :if ($Num = 0) do={
+    :return "";
+  }
+
+  :local Return "";
+  :for I from=1 to=$Num do={
+    :set Return ($Return . $Str);
+  }
+  :return $Return;
+}
+
+# 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;
+}
+
+# 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;
+}
+
+# return a brief commit description
+:set CommitBrief do={
+  :global CommitId;
+  :global CommitInfo;
+
+  :if ($CommitId = "unknown") do={
+    :return "unknown";
+  }
+
+  :return ($CommitInfo . "/" . [ :pick $CommitId 0 8 ]);
+}
+
+# get readable device info
+:set DeviceInfo do={
+  :global ExpectedConfigVersion;
+  :global Identity;
+
+  :global CommitBrief;
+  :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 Snmp [ /snmp/get ];
+  :local Update [ /system/package/update/get ];
+
+  :return ( \
+    [ $FormatLine "Hostname" $Identity ] . "\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") ] . \
+       [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
+           (" " . $RouterBoard->"revision") ] . "\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" && \
+        $Update->"installed-version" != $Update->"latest-version") \
+      ([ $FormatLine "    Available" ($Update->"latest-version") ] . "\n") ] . \
+    [ $IfThenElse ($RouterBoard->"routerboard" = true && \
+        $RouterBoard->"current-firmware" != $RouterBoard->"upgrade-firmware") \
+      ([ $FormatLine "    Firmware" ($RouterBoard->"current-firmware") ] . "\n") ] . \
+    "RouterOS-Scripts:\n" . \
+    [ $FormatLine "    Commit" [ $CommitBrief ] ] . "\n" . \
+    [ $FormatLine "    Version" $ExpectedConfigVersion ]);
+}
+
+# convert line endings, DOS -> UNIX
+:set Dos2Unix do={
+  :return [ :tolf [ :tostr $1 ] ];
+}
+
+# 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 FileExists;
+  :global LogPrint;
+  :global MkDir;
+  :global RmFile;
+  :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={
+    $LogPrint warning $0 ("Failed creating directory, not downloading package.");
+    :return false;
+  }
+
+  :if ([ $FileExists $PkgDest "package" ] = true) do={
+    $LogPrint info $0 ("Package file " . $PkgName . " already exists.");
+    :return true;
+  }
+
+  :if ([ $CertificateAvailable "ISRG Root X1" "fetch" ] = false) do={
+    $LogPrint error $0 ("Downloading required certificate failed.");
+    :return false;
+  }
+
+  :local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile);
+  $LogPrint info $0 ("Downloading package file '" . $PkgName . "'...");
+  $LogPrint debug $0 ("... from url: " . $Url);
+
+  :onerror Err {
+    /tool/fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
+    $WaitForFile $PkgDest;
+  } do={
+    $LogPrint warning $0 ("Downloading package file '" . $PkgName . "' failed: " . $Err);
+    :return false;
+  }
+
+  :if ([ $FileExists $PkgDest "package" ] = false) do={
+    $LogPrint warning $0 ("Downloaded file is not a package, removing.");
+    $RmFile $PkgDest;
+    :return false;
+  }
+
+  :return true;
+}
+
+# return either first (if "true") or second
+:set EitherOr do={
+  :global IfThenElse;
+
+  :if ([ :typeof $1 ] = "num") do={
+    :return [ $IfThenElse ($1 != 0) $1 $2 ];
+  }
+  :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 ];
+}
+
+# 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;
+}
+
+# wrapper for $ExitOnError with additional parameter
+:set ExitError do={
+  :local ExitOK [ :tostr $1 ];
+  :local Name   [ :tostr $2 ];
+  :local Error  [ :tostr $3 ];
+
+  :global ExitOnError;
+
+  :if ($ExitOK = "false") do={
+    $ExitOnError $Name $Error;
+  }
+}
+
+# simple macro to print error message on unintentional error
+:set ExitOnError do={
+  :local Name   [ :tostr $1 ];
+  :local Error  [ :tostr $2 ];
+
+  :global IfThenElse;
+  :global LogPrint;
+
+  $LogPrint error $Name ([ $IfThenElse ([ :pick $Name 0 1 ] = "\$") \
+      "Function" "Script" ] . " '" . $Name . "' exited with error" . \
+      [ $IfThenElse (!($Error ~ "^(|true|false)\$")) (": " . $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 ]);
+  :onerror Err {
+    /tool/fetch check-certificate=$CheckCert $Url dst-path=$FileName \
+      http-header-field=({ [ $FetchUserAgentStr $ScriptName ] }) as-value;
+  } do={
+    :if ([ $WaitForFile $FileName 500ms ] = true) do={
+      $RmFile $FileName;
+    }
+    $LogPrint debug $0 ("Failed downloading from " . $Url . " - " . $Err);
+    $RmDir $DirName;
+    :return false;
+  }
+
+  :if ([ $WaitForFile $FileName 5s ] = false) do={
+    $LogPrint debug $0 ("The file downloaded from " . $Url . " did not show up.");
+    :return false;
+  }
+
+  :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 ];
+
+  :global CommitBrief;
+  :global IfThenElse;
+
+  :local Resource [ /system/resource/get ];
+
+  :return ("User-Agent: Mikrotik/" . $Resource->"version" . " " . $Resource->"architecture-name" . \
+    " " . $Caller . "/Fetch (https://rsc.eworm.de/; " . [ $CommitBrief ] . ")");
+}
+
+# check for existence of file, optionally with type
+:set FileExists do={
+  :local FileName [ :tostr $1 ];
+  :local Type     [ :tostr $2 ];
+
+  :global FileGet;
+
+  :local FileVal [ $FileGet $FileName ];
+  :if ($FileVal = false) do={
+    :return false;
+  }
+
+  :if ([ :len ($FileVal->"size") ] = 0) do={
+    :return false;
+  }
+
+  :if ([ :len $Type ] = 0 || $FileVal->"type" = $Type) do={
+    :return true;
+  }
+
+  :return false;
+}
+
+# get file properties in array, or false on error
+:set FileGet do={
+  :local FileName [ :tostr $1 ];
+
+  :global WaitForFile;
+
+  :if ([ $WaitForFile $FileName 0s ] = false) do={
+    :return false;
+  }
+
+  :local FileVal false;
+  :do {
+    :set FileVal [ /file/get $FileName ];
+  } on-error={ }
+
+  :return $FileVal;
+}
+
+# format a line for output
+:set FormatLine do={
+  :local Key    [ :tostr $1 ];
+  :local Value  [ :tostr $2 ];
+  :local Indent [ :tonum $3 ];
+  :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={
+    :set Return ($Return . "\n" . [ :pick $Spaces 0 $Indent ] . $Value);
+  } else={
+    :set Return ($Return . [ :pick $Spaces 0 ($Indent - [ :len $Return ]) ] . $Value);
+  }
+
+  :return $Return;
+}
+
+# format multiple lines for output
+:set FormatMultiLines do={
+  :local Key    [ :tostr   $1 ];
+  :local Values [ :toarray $2 ];
+  :local Indent [ :tonum   $3 ];
+  :local Return;
+
+  :global FormatLine;
+
+  :set Return [ $FormatLine $Key ($Values->0) $Indent ];
+  :foreach Value in=[ :pick $Values 1 [ :len $Values ] ] do={
+    :set Return ($Return . "\n" . [ $FormatLine "" $Value $Indent ]);
+  }
+
+  :return $Return;
+}
+
+# get MAC vendor
+:set GetMacVendor do={
+  :local Mac [ :tostr $1 ];
+
+  :global CertificateAvailable;
+  :global IsMacLocallyAdministered;
+  :global LogPrint;
+
+  :if ([ $IsMacLocallyAdministered $Mac ] = true) do={
+    :return "locally administered";
+  }
+
+  :do {
+    :if ([ $CertificateAvailable "GTS Root R4" "fetch" ] = 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");
+    :return $Vendor;
+  } on-error={
+    :onerror Err {
+      /tool/fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \
+        output=none as-value;
+      $LogPrint debug $0 ("The mac vendor is not known in database.");
+    } do={
+      $LogPrint warning $0 ("Failed getting mac vendor: " . $Err);
+    }
+    :return "unknown vendor";
+  }
+}
+
+# generate random 20 chars alphabetical (A-Z & a-z) and numerical (0-9)
+:set GetRandom20CharAlNum do={
+  :global EitherOr;
+
+  :return [ :rndstr length=[ $EitherOr [ :tonum $1 ] 20 ] from="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ];
+}
+
+# generate random 20 chars hex (0-9 and a-f)
+:set GetRandom20CharHex do={
+  :global EitherOr;
+
+  :return [ :rndstr length=[ $EitherOr [ :tonum $1 ] 20 ] from="0123456789abcdef" ];
+}
+
+# generate random number
+:set GetRandomNumber do={
+  :global EitherOr;
+
+  :return [ :rndnum from=0 to=[ $EitherOr [ :tonum $1 ] 4294967295 ] ];
+}
+
+# return first line that matches a pattern
+:set Grep do={
+  :local Input  ([ :tostr $1 ] . "\n");
+  :local Pattern [ :tostr $2 ];
+
+  :if ([ :typeof [ :find $Input $Pattern ] ] = "nil") do={
+    :return [];
+  }
+
+  :do {
+    # Do *NOT* try :deserialize here to split lines. It can fail with
+    # scripts, and it *does* fail with global-functions!
+    :local Len [ :find $Input "\n" ];
+    :local Line [ :pick $Input 0 $Len ];
+    :if ([ :typeof [ :find $Line $Pattern ] ] = "num") do={
+      :return $Line;
+    }
+    :set Input [ :pick $Input ($Len + 1) [ :len $Input ] ];
+  } while=([ :len $Input ] > 0);
+
+  :return [];
+}
+
+# convert from hex (string) to num
+:set HexToNum do={
+  :local Input [ :tostr $1 ];
+
+  :global HexToNum;
+
+  :if ([ :pick $Input 0 ] = "*") do={
+    :return [ $HexToNum [ :pick  $Input 1 [ :len $Input ] ] ];
+  }
+
+  :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)
+: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={
+  :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 mac address is locally administered
+:set IsMacLocallyAdministered do={
+  :if ([ :tonum ("0x" . [ :pick $1 0 [ :find $1 ":" ] ]) ] & 2 = 2) do={
+    :return true;
+  }
+  :return false;
+}
+
+# check if system time is sync
+:set IsTimeSync do={
+  :global IsTimeSyncCached;
+  :global IsTimeSyncResetNtp;
+
+  :global LogPrintOnce;
+
+  :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;
+    }
+
+    :local Uptime [ /system/resource/get uptime ];
+    :if ([ :typeof $IsTimeSyncResetNtp ] = "nothing") do={
+      :set IsTimeSyncResetNtp $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;
+    /system/ntp/client/set enabled=yes;
+    :return false;
+  }
+
+  :if ([ /system/license/get ]->"level" = "free" || \
+       [ /system/resource/get ]->"board-name" = "x86") do={
+    $LogPrintOnce debug $0 ("No ntp client configured, relying on RTC for CHR free license and x86.");
+    :return true;
+  }
+
+  :if ([ /ip/cloud/get update-time ] = true) do={
+    :if ([ :typeof [ /ip/cloud/get public-address ] ] = "ip") do={
+      :set IsTimeSyncCached true;
+      :return true;
+    }
+    :return false;
+  }
+
+  $LogPrintOnce debug $0 ("No time source configured! Returning gracefully...");
+  :return true;
+}
+
+# log and print with same text
+:set LogPrint do={
+  :local Severity [ :tostr $1 ];
+  :local Name     [ :tostr $2 ];
+  :local Message  [ :tostr $3 ];
+
+  :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={
+    :put ([ $PrintSeverity $Severity ] . ": " . $Message);
+  }
+}
+
+# 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 CleanFilePath;
+  :global FileGet;
+  :global LogPrint;
+  :global RmDir;
+  :global WaitForFile;
+
+  :local MkTmpfs do={
+    :global LogPrint;
+    :global WaitForFile;
+
+    :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;
+    }
+
+    $LogPrint info $0 ("Creating disk of type tmpfs.");
+    $RmDir "tmpfs";
+    :onerror Err {
+      /disk/add slot=tmpfs type=tmpfs tmpfs-max-size=([ /system/resource/get total-memory ] / 3);
+      $WaitForFile "tmpfs";
+    } do={
+      $LogPrint warning $0 ("Creating disk of type tmpfs failed: " . $Err);
+      :return false;
+    }
+    :return true;
+  }
+
+  :set Path [ $CleanFilePath $Path ];
+
+  :if ($Path = "") do={
+    :return true;
+  }
+
+  $LogPrint debug $0 ("Making directory: " . $Path);
+
+  :local PathVal [ $FileGet $Path ];
+  :if ($PathVal->"type" = "directory") do={
+    $LogPrint debug $0 ("... which already exists.");
+    :return true;
+  }
+
+  :if ([ :pick $Path 0 5 ] = "tmpfs") do={
+    :if ([ $MkTmpfs ] = false) do={
+      :return false;
+    }
+  }
+
+  :onerror Err {
+    /file/add type="directory" name=$Path;
+    $WaitForFile $Path;
+  } do={
+    $LogPrint warning $0 ("Making directory '" . $Path . "' failed: " . $Err);
+    :return false;
+  }
+
+  :return true;
+}
+
+# return an IPv4 netmask for CIDR
+:set NetMask4 do={
+  :local CIDR [ :tonum $1 ];
+
+  :return ((255.255.255.255 << (32 - $CIDR)) & 255.255.255.255);
+}
+
+# return an IPv6 netmask for CIDR
+:set NetMask6 do={
+  :local CIDR [ :tostr $1 ];
+
+  :global IfThenElse;
+  :global MAX;
+  :global MIN;
+
+  :global NetMask6Cache;
+
+  :if ([ :typeof ($NetMask6Cache->$CIDR) ] = "ip6") do={
+    :return ($NetMask6Cache->$CIDR);
+  }
+
+  :if ([ :typeof $NetMask6Cache ] = "nothing") do={
+    :set NetMask6Cache ({});
+  }
+
+  :local Mask "";
+  :for I from=0 to=7 do={
+    :set Mask ($Mask . \
+      [ :convert from=num to=hex (0xffff - (0xffff >> [ :tonum [ $MIN [ $MAX ($CIDR - (16 * $I)) 0 ] 16 ] ])) ] . \
+      [ $IfThenElse ($I < 7) ":" ]);
+  }
+  :set Mask [ :toip6 $Mask ];
+  :set ($NetMask6Cache->$CIDR) $Mask;
+  :return $Mask;
+}
+
+# prepare NotificationFunctions array
+:if ([ :typeof $NotificationFunctions ] != "array") do={
+  :set NotificationFunctions ({});
+}
+
+# parse the date and return a named array
+:set ParseDate do={
+  :local Date [ :tostr $1 ];
+
+  :return ({ "year"=[ :tonum [ :pick $Date 0 4 ] ];
+            "month"=[ :tonum [ :pick $Date 5 7 ] ];
+              "day"=[ :tonum [ :pick $Date 8 10 ] ] });
+}
+
+# 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={
+      :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;
+    }
+  }
+  :return $Result;
+}
+
+# print lines with trailing carriage return
+:set PrettyPrint do={
+  :put [ :tocrlf [ :tostr $1 ] ];
+}
+
+# strip protocol from from url string
+:set ProtocolStrip do={
+  :local Input [ :tostr $1 ];
+
+  :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;
+
+  :if ($Time = 0) do={
+    :return false;
+  }
+
+  :delay ([ $MAX 10 [ $GetRandomNumber ([ :tonsec [ :totime ($Time . [ $EitherOr $Unit "s" ]) ] ] / 1000000) ] ] . "ms");
+}
+
+# check for required RouterOS version
+:set RequiredRouterOS do={
+  :local Caller   [ :tostr $1 ];
+  :local Required [ :tostr $2 ];
+  :local Warn     [ :tostr $3 ];
+
+  :global IfThenElse;
+  :global LogPrint;
+  :global VersionToNum;
+
+  :if (!($Required ~ "^\\d+\\.\\d+((alpha|beta|rc|\\.)\\d+|)\$")) do={
+    $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={
+      $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 FileGet;
+  :global LogPrint;
+
+  $LogPrint debug $0 ("Removing directory: ". $DirName);
+
+  :local DirVal [ $FileGet $DirName ];
+  :if ($DirVal = false) do={
+    $LogPrint debug $0 ("... which does not exist.");
+    :return true;
+  }
+
+  :if ($DirVal->"type" != "directory") do={
+    $LogPrint error $0 ("Directory '" . $DirName . "' is not a directory.");
+    :return false;
+  }
+
+  :onerror Err {
+    /file/remove $DirName;
+  } do={
+    $LogPrint error $0 ("Removing directory '" . $DirName . "' failed: " . $Err);
+    :return false;
+  }
+  :return true;
+}
+
+# remove file
+:set RmFile do={
+  :local FileName [ :tostr $1 ];
+
+  :global FileGet;
+  :global LogPrint;
+
+  $LogPrint debug $0 ("Removing file: ". $FileName);
+
+  :local FileVal [ $FileGet $FileName ];
+  :if ($FileVal = false) do={
+    $LogPrint debug $0 ("... which does not exist.");
+    :return true;
+  }
+
+  :if ($FileVal->"type" = "directory" || $FileVal->"type" = "disk") do={
+    $LogPrint error $0 ("File '" . $FileName . "' is not a file.");
+    :return false;
+  }
+
+  :onerror Err {
+    /file/remove $FileName;
+  } do={
+    $LogPrint error $0 ("Removing file '" . $FileName . "' failed: " . $Err);
+    :return false;
+  }
+  :return true;
+}
+
+# check if script is run from terminal
+:set ScriptFromTerminal do={
+  :local Script [ :tostr $1 ];
+
+  :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 ];
+    :while ([ :typeof ($Job->"parent") ] = "id") do={
+      :set Job [ /system/script/job/get [ find where .id=($Job->"parent") ] ];
+    }
+    :if (($Job->"type") = "login") do={
+      $LogPrint debug $0 ("Script " . $Script . " started from terminal.");
+      :return true;
+    }
+  }
+
+  $LogPrint debug $0 ("Script " . $Script . " NOT started from terminal.");
+  :return false;
+}
+
+# install new scripts, update existing scripts
+:set ScriptInstallUpdate do={ :onerror Err {
+  :local Scripts    [ :toarray $1 ];
+  :local NewComment [ :tostr   $2 ];
+
+  :global CommitId;
+  :global ExpectedConfigVersion;
+  :global GlobalConfigReady;
+  :global GlobalFunctionsReady;
+  :global Identity;
+  :global IDonate;
+  :global NoNewsAndChangesNotification;
+  :global ScriptUpdatesBaseUrl;
+  :global ScriptUpdatesCRLF;
+  :global ScriptUpdatesUrlSuffix;
+
+  :global CertificateAvailable;
+  :global EitherOr;
+  :global FetchUserAgentStr;
+  :global Grep;
+  :global IfThenElse;
+  :global LogPrint;
+  :global LogPrintOnce;
+  :global ParseKeyValueStore;
+  :global RequiredRouterOS;
+  :global SendNotification2;
+  :global SymbolForNotification;
+  :global ValidateSyntax;
+
+  :if ([ $CertificateAvailable "Root YE" "fetch" ] = 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={
+      $LogPrint warning $0 ("Requested to add script '" . $Script . "', but that exists already!");
+    } else={
+      $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 ReloadGlobal 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 ScriptInfo [ $ParseKeyValueStore ($ScriptVal->"comment") ];
+    :local SourceNew;
+
+    :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={
+        $LogPrint warning $0 ("Policies differ for script '" . $ScriptVal->"name" . \
+          "' and its scheduler '" . $SchedulerVal->"name" . "'!");
+      }
+    }
+
+    :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") "fetch" ] = false) do={
+          $LogPrint warning $0 ("Downloading certificate failed, trying without.");
+        }
+      }
+
+      :onerror Err {
+        :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") ];
+        }
+      } do={
+        $LogPrint warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "': " . $Err);
+        :if ($Err != "Fetch failed with status 404") do={
+          :error false;
+        }
+
+        :if ($ScriptVal->"source" = "#!rsc by RouterOS\n") do={
+          $LogPrint warning $0 ("Removing dummy. Typo on installation?");
+          /system/script/remove $Script;
+          :error false;
+        }
+        :if ([ :len ($ScriptInfo->"base-url") ] = 0 && [ :len ($ScriptInfo->"url-suffix") ] = 0 && \
+             [ :len $CheckSum ] = 0) do={
+          $LogPrintOnce warning $0 \
+              ("Added the script manually? Skip updates with 'ignore=true' in comment.");
+        }
+        :error 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" || \
+           $ScriptVal->"name" = "global-functions" || \
+           $ScriptVal->"name" ~ ("^(global-functions\\.d|mod)/.")) do={
+        :set ReloadGlobal true;
+      }
+    } on-error={ }
+  }
+
+  :if ($ReloadGlobal = true) do={
+    $LogPrint info $0 ("Reloading global configuration and functions.");
+    :set GlobalConfigReady false;
+    :set GlobalFunctionsReady false;
+    :delay 1s;
+
+    :onerror Err {
+      /system/script/run global-config;
+      /system/script/run global-functions;
+    } do={
+      $LogPrint error $0 ("Reloading global configuration and functions failed! " . $Err);
+    }
+  }
+
+  :if ($ExpectedConfigVersionBefore > $ExpectedConfigVersion) do={
+    $LogPrint warning $0 ("The configuration version decreased from " . \
+      $ExpectedConfigVersionBefore . " to " . $ExpectedConfigVersion . \
+      ". Installed an older version?");
+  }
+
+  :if ($ExpectedConfigVersionBefore < $ExpectedConfigVersion) do={
+    :global GlobalConfigChanges;
+    :global GlobalConfigMigration;
+    :local ChangeLogCode;
+
+    :onerror Err {
+      :local Url ($ScriptUpdatesBaseUrl . "news-and-changes.rsc" . $ScriptUpdatesUrlSuffix);
+      $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");
+      }
+    } do={
+      $LogPrint warning $0 ("Failed fetching news, changes and migration: " . $Err);
+    }
+
+    :if ([ :len $ChangeLogCode ] > 0) do={
+      :if ([ $ValidateSyntax $ChangeLogCode ] = true) do={
+        :onerror Err {
+          [ :parse $ChangeLogCode ];
+        } do={
+          $LogPrint warning $0 ("The changelog failed to run: " . $Err);
+        }
+      } else={
+        $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 ]);
+        :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);
+          :onerror Err {
+            [ :parse $Migration ];
+          } do={
+            $LogPrint warning $0 ("Migration code for change " . $I . " failed to run: " . $Err);
+          }
+        } 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.");
+    $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 " . \
+            [ $SymbolForNotification "pushpin" "*" ] . $Change);
+        $LogPrint info $0 ("Change " . $I . ": " . $Change);
+      }
+    } else={
+      :set NotificationMessage ($NotificationMessage . "\n\nNews and changes are not available.");
+    }
+
+    :if ($NoNewsAndChangesNotification != true) do={
+      :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://rsc.eworm.de/#donate";
+      }
+
+      $SendNotification2 ({ origin=$0; \
+        subject=([ $SymbolForNotification "pushpin" ] . "News and configuration changes"); \
+        message=$NotificationMessage; link=$Link });
+    }
+
+    :set GlobalConfigChanges;
+    :set GlobalConfigMigration;
+  }
+} do={
+  :global ExitOnError; $ExitOnError $0 $Err;
+} }
+
+# lock script against multiple invocation
+:set ScriptLock do={
+  :local Script  [ :tostr  $1 ];
+  :local WaitMax [ :totime $2 ];
+
+  :global GetRandom20CharAlNum;
+  :global IfThenElse;
+  :global LogPrint;
+
+  :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 ([ :typeof $WaitMax ] = "nil" ) do={
+    :set WaitMax 0s;
+  }
+
+  :if ([ :len [ /system/script/find where name=$Script ] ] = 0) do={
+    $LogPrint error $0 ("A script named '" . $Script . "' does not exist!");
+    :error false;
+  }
+
+  :if ([ $JobCount $Script ] = 0) do={
+    $LogPrint error $0 ("No script '" . $Script . "' is running!");
+    :error false;
+  }
+
+  :if ([ $TicketCount $Script ] >= [ $JobCount $Script ]) do={
+    $LogPrint error $0 ("More tickets than running scripts '" . $Script . "', resetting!");
+    :set ($ScriptLockOrder->$Script) ({});
+    /system/script/job/remove [ find where script=$Script ];
+  }
+
+  :local MyTicket [ $GetRandom20CharAlNum 6 ];
+  $AddTicket $Script $MyTicket;
+
+  :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={
+    $RemoveTicket $Script $MyTicket;
+    $CleanupTickets $Script;
+    :return true;
+  }
+
+  $RemoveTicket $Script $MyTicket;
+  $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={ :onerror Err {
+  :global SendNotification2;
+
+  $SendNotification2 ({ origin=$0; subject=$1; message=$2; link=$3; silent=$4 });
+} do={
+  :global ExitOnError; $ExitOnError $0 $Err;
+} }
+
+# 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 Name [ :tostr $1 ];
+
+  :global EitherOr;
+  :global LogPrintOnce;
+
+  :global SymbolsExtra;
+
+  :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";
+    "chart-increasing"="\F0\9F\93\88";
+    "cloud"="\E2\98\81";
+    "cross-mark"="\E2\9D\8C";
+    "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";
+    "scroll"="\F0\9F\93\9C";
+    "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";
+    "star"="\E2\AD\90";
+    "warning-sign"="\E2\9A\A0";
+    "white-heavy-check-mark"="\E2\9C\85"
+  }, $SymbolsExtra);
+
+  :local Magic [ :pick [ /system/clock/get date ] 4 10 ];
+  :local Special {
+    "information-04-01"="\F0\9F\9A\BB";
+    "large-orange-circle-04-01"="\F0\9F\8D\8A";
+    "large-orange-circle-10-31"="\F0\9F\8E\83";
+    "large-red-circle-04-01"="\F0\9F\8D\92" };
+
+  :if ([ :len ($Symbols->$Name) ] = 0) do={
+    $LogPrintOnce warning $0 ("No symbol available for name '" . $Name . "'!");
+    :return "";
+  }
+
+  :return ([ $EitherOr ($Special->($Name . $Magic)) ($Symbols->$Name) ] . "\EF\B8\8F");
+}
+
+# return symbol for notification
+:set SymbolForNotification do={
+  :global NotificationsWithSymbols;
+  :global SymbolByUnicodeName;
+  :global IfThenElse;
+
+  :if ($NotificationsWithSymbols != true) do={
+    :return [ $IfThenElse ([ :len $2 ] > 0) ([ :tostr $2 ] . " ") "" ];
+  }
+  :local Return "";
+  :foreach Symbol in=[ :toarray $1 ] do={
+    :set Return ($Return . [ $SymbolByUnicodeName $Symbol ]);
+  }
+  :return ($Return . " ");
+}
+
+# convert line endings, UNIX -> DOS
+:set Unix2Dos do={
+  :return [ :tocrlf [ :tostr $1 ] ];
+}
+
+# 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 ];
+
+  :global LogPrint;
+
+  :onerror Err {
+    [ :parse (":local Validate do={\n" . $Code . "\n}") ];
+  } do={
+    $LogPrint debug $0 ("Valdation failed: " . $Err);
+    :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 $Input "." "," ];
+  :foreach I in={ "zero"; "alpha"; "beta"; "rc" } do={
+    :set Input [ $CharacterReplace $Input $I ("," . $I . ",") ];
+  }
+
+  :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 = "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); }
+      }
+    }
+    :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 ];
+  :local WaitTime [ :totime $2 ];
+
+  :global CleanFilePath;
+  :global EitherOr;
+  :global MAX;
+
+  :set FileName [ $CleanFilePath $FileName ];
+  :local Delay ([ $MAX [ $EitherOr $WaitTime 2s ] 100ms ] / 9);
+
+  :do {
+    :retry {
+      /file/get $FileName;
+      :return true;
+    } delay=$Delay max=10;
+  } on-error={ }
+
+  :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)
+: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 ~ "^(global-functions\\.d|mod)/." ] do={
+  :local ScriptVal [ /system/script/get $Script ];
+  :if ([ $ValidateSyntax ($ScriptVal->"source") ] = true) do={
+    :onerror Err {
+      /system/script/run $Script;
+    } do={
+      $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed to run: " . $Err);
+    }
+  } else={
+    $LogPrint error $0 ("Module '" . $ScriptVal->"name" . "' failed syntax validation, skipping.");
+  }
+}
+
+# add (and fix) global scripts scheduler
+/system/scheduler {
+  :local OnEvent "/system/script { run global-config; run global-functions; }";
+  :if ([ :len [ find where name="global-scripts" ] ] = 0) do={
+    add name="global-scripts" start-time=startup;
+  }
+  set on-event=$OnEvent [ find where name="global-scripts" on-event!=$OnEvent ];
+  enable [ find where name="global-scripts" disabled ];
+}
+
+# Log success
+:local Resource [ /system/resource/get ];
+$LogPrintOnce info $ScriptName ("Loaded " . \
+    [ $IfThenElse ($CommitId != "unknown") ([ $CommitBrief ] . " ") ] . \
+    "on " . $Resource->"board-name" . " with RouterOS " . $Resource->"version" . ".");
+
+# signal we are ready
+:set GlobalFunctionsReady true;
index e4e3633c3ea2ca059dc029fa0df4c49b2e167ce7..0cf32cf55543035377477f13c7cb665f2aeff5e2 100644 (file)
@@ -5,10 +5,10 @@
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta http-equiv="content-type" content="text/html; charset=UTF-8">
-  <link rel="stylesheet" type="text/css" href="/.include/css/style.css">
-  <link rel="icon" type="image/png" href="/.include/img/logo.png">
-  <script src="/.include/js/clipboard.js"></script>
-  <script src="/.include/js/color.js"></script>
+  <link rel="stylesheet" type="text/css" href="/_inc/css/style.css">
+  <link rel="icon" type="image/png" href="/_inc/img/logo.png">
+  <script src="/_inc/js/clipboard.js"></script>
+  <script src="/_inc/js/color.js"></script>
 </head>
 
 <body>
@@ -38,7 +38,7 @@
       <div class="root">
 
       <h1>Logo Color Changer</h1>
-      <embed class="logo" src="/.include/img/logo.svg" width="192" height="192" type="image/svg+xml">
+      <embed class="logo" src="/_inc/img/logo.svg" width="192" height="192" type="image/svg+xml">
 
       <p>Select the colors here:
         <input id="color1" type="color" value="#222222" onchange="color();">
index cee377a9af8e2c9aba62c9c308d9cb38dc8cfebe..e23aec63aaa0393b92aa7acc1cf2440feec8bffa 100644 (file)
@@ -1,9 +1,13 @@
 #!rsc by Vados
-# RouterOS function-collection
-# DateTime Functions
+# RouterOS script: mod/AM-DateTime
+# Script comment: DateTime format functions
+#
+#
+# requires RouterOS, version=7.21
+# requires device-mode, fetch, scheduler
 
 :global CurrentDateTime;
-:global FullDateTimeStamp;
+:global DateTimeStamp;
 :global MiniDateTimeStamp;
 
 :set CurrentDateTime do={
@@ -16,7 +20,9 @@
        } else={:set currDate $rawDate}
 :return ($currDate . " " . $currTime);
 }
-:set FullDateTimeStamp do={
+
+:set DateTimeStamp do={
+    :local getFmt [:tostr $1]
     /system clock
     :local vdate [get date]
     :local vtime [get time]
     :local s    [:tonum $ss]
     :local Z    "$gmtSg$gmtHr:$gmtMn"
     :local Unix (((((($totd * 24) + $H) * 60) + $m) * 60) + $s - $vgmt)
-#    :return "$yyyy-$MM-$dd\54$HH:$mm:$ss$Z $Www (YD: $YD) (MD: $MD) (WD: $WD) $Leap $Unix"
-    :return "$yyyy$MM$dd$HH$mm$ss"
+    :if ($getFmt = "Min") do={
+        :return "$yyyy$MM$dd$HH$mm$ss";
+    } else={
+        :return "$yyyy$MM$dd$HH$mm$ss";
+    }
 }
+
 :set MiniDateTimeStamp do={
     /system clock
     :local vdate [get date]
similarity index 89%
rename from html/AM-GitBackup.rsc
rename to html/mod/AM-GitBackup.rsc
index 368b6d4fd788b9baf574b9e326b1c4a58431a2e6..7d1fbb903d26155023044e19beb3b64d9acde629 100644 (file)
@@ -1,4 +1,9 @@
-# MikroTik Git Config Backup
+#!rsc by Vados
+# RouterOS script: mod/AM-GitBackup
+# Script comment: MikroTik Git Config Backup
+# Copyright (c) 2005-2026 Vados <vados@vados.ru>
+#
+# 
 # Edit the three variables below, then add this script under System > Scripts
 
 :local serverUrl "http://192.168.1.10:8080"
diff --git a/html/mod/AM-IPbridge.rsc b/html/mod/AM-IPbridge.rsc
new file mode 100644 (file)
index 0000000..c0ecbd8
--- /dev/null
@@ -0,0 +1,30 @@
+#!rsc by Vados
+# RouterOS script: mod/AM-IPbridge
+# Script comment: Enable or disable ip addresses based on bridge port state
+# Copyright (c) 2005-2026 Vados <vados@vados.ru>
+#
+#
+# requires RouterOS, version=7.19
+# requires device-mode, fetch
+#
+:local ScriptName [ :jobname ];
+
+# Add this Script scheduler
+:if ([ :len [ /system/scheduler/find where name=$ScriptName ] ] = 0) do={
+:log warning ("SystemScheduler for script: " . $ScriptName . " is NOT SET! Fix It Now!");
+:put "Add or/and fix $ScriptName Script scheduler";
+:local OnEvent "/system/script/run $ScriptName;";
+:local Comment "Every Day Check For Fake Bridges";
+/system/scheduler/add name=$ScriptName comment=$Comment start-time=startup interval=1d on-event=$OnEvent;
+}
+
+:foreach Bridge in=[ /interface/bridge/find ] do={
+  :local BrName [ /interface/bridge/get $Bridge name ];
+  :if ([ :len [ /interface/bridge/port/find where bridge=$BrName ] ] > 0) do={
+    :if ([ :len [ /interface/bridge/port/find where bridge=$BrName and inactive=no ] ] = 0) do={
+      /ip/address/disable [ find where !dynamic interface=$BrName ];
+    } else={
+      /ip/address/enable [ find where !dynamic interface=$BrName ];
+    }
+  }
+}
similarity index 53%
rename from html/mod/ipcalc.rsc
rename to html/mod/AM-IPcalc.rsc
index 2aad0cb0997b8ef667ddd2b89bd7a029b38bfd93..836df4fcd47b080d5991305517b720b19e7bcccf 100644 (file)
@@ -1,19 +1,48 @@
 #!rsc by Vados
-# RouterOS script: ipcalc
+# RouterOS script: mod/AM-IPcalc
 # Script comment: ip address calculation
 #
 #
 # requires RouterOS, version=7.19
 
-:global IPCalc;
-:global IPCalcReturn;
+:local IPCalc;
 
 # print netmask, network, min host, max host and broadcast
 :set IPCalc do={ :onerror Err {
   :local Input [ :tostr $1 ];
-  :global FormatLine;
-  :global IPCalcReturn;
+  :local FormatLine;
+  :local IPCalcReturn;
   :local Values [ $IPCalcReturn $1 ];
+  :set FormatLine do={
+    :local Key    [ :tostr $1 ];
+    :local Value  [ :tostr $2 ];
+    :local Indent [ :tonum $3 ];
+    :local Spaces;
+    :local Return "";
+    :local CharacterMultiply;
+    :local EitherOr;
+    :set CharacterMultiply do={
+      :local Str [ :tostr $1 ];
+      :local Num [ :tonum $2 ];
+      :local Return "";
+      :if ($Num = 0) do={:return ""}
+      :for I from=1 to=$Num do={:set Return ($Return . $Str)}
+      :return $Return;
+    }
+    :set EitherOr do={
+      :local IfThenElse;
+      :set IfThenElse do={:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={ :return $2 }; :return $3; }
+      :if ([ :typeof $1 ] = "num") do={:return [ $IfThenElse ($1 != 0) $1 $2 ]}
+      :if ([ :typeof $1 ] = "time") do={:return [ $IfThenElse ($1 > 0s) $1 $2 ]}
+      :return [ $IfThenElse ([ :len [ :tostr $1 ] ] > 0) $1 $2 ];
+    }
+    :set Indent [ $EitherOr $Indent 16 ];
+    :local Spaces [ $CharacterMultiply " " $Indent ];
+    :if ([ :len $Key ] > 0) do={ :set Return ($Key . ":")}
+    :if ([ :len $Key ] > ($Indent - 2)) do={:set Return ($Return . "\n" . [ :pick $Spaces 0 $Indent ] . $Value);
+    } else={:set Return ($Return . [ :pick $Spaces 0 ($Indent - [ :len $Return ]) ] . $Value)}
+    :return $Return;
+  }
   :put [ :tocrlf ( \
     [ $FormatLine "Address" ($Values->"address") ] . "\n" . \
     [ $FormatLine "Netmask" ($Values->"netmask") ] . "\n" . \
@@ -22,7 +51,7 @@
     [ $FormatLine "HostMax" ($Values->"hostmax") ] . "\n" . \
     [ $FormatLine "Broadcast" ($Values->"broadcast") ]) ];
 } do={
-  :global ExitOnError; $ExitOnError $0 $Err;
+  :log error ("Error:" );
 } }
 
 # calculate and return netmask, network, min host, max host and broadcast
index 9dfc1a4ef59fdb0d45745c9b3558ee74b2bb9ecd..4798bb5908fa4699706dc72b680302351b05172f 100644 (file)
@@ -1,7 +1,8 @@
 #!rsc by Vados
-# RouterOS script: RunOnce
+# RouterOS script: mod/AM-RunOnce
 # Script comment: Download script and run it once
 #
+#
 # requires RouterOS, version=7.19
 
 :global ScriptRunOnce;
index 17ef3aff39262520e0bd6c1c259d3f85f43d5dbf..6c7af2b405b8ef577ed11ec125a6199c4a015c00 100644 (file)
@@ -1,5 +1,5 @@
 #!rsc by Vados
-# RouterOS script: AM-TG-Bot
+# RouterOS script: mod/AM-TG-Bot
 # Script comment: Use Telegram to chat with your Router and send commands
 # Copyright (c) 2007-2026 Vados <vados@vados.ru>
 #
index fdcc947a1c0a7da3983da846e729bd249cb3e636..1190760b9d247e75dedaa2787978c33d39e0f693 100644 (file)
@@ -1,5 +1,5 @@
 #!rsc by Vados
-# RouterOS script: AM-TG-Notifications
+# RouterOS script: mod/AM-TG-Notifications
 # Script comment: Script send Notifications in Telegram
 # Copyright (c) 2007-2026 Vados <vados@vados.ru>
 #
similarity index 95%
rename from html/Update-Certificates.rsc
rename to html/mod/AM-UpdateCertsCustom.rsc
index c4012ca1bb29a65957b4633270125a75095ea912..b5351a43c3f35a120eb676937a9cb5f9c374ffd1 100644 (file)
@@ -1,10 +1,9 @@
 #!rsc by Vados
-# RouterOS script: Update-Certificates
-# Script comment: Certificates CloudFlare, Quad9
+# RouterOS script: mod/AM-UpdateCertsCustom
+# Script comment: Custom update Mikrotiks Certificates (Google, CloudFlare, Quad9...)
 # Copyright (c) 2005-2026 Vados <vados@vados.ru>
 #
 #
-#
 # requires RouterOS, version=7.19
 # requires device-mode, fetch
 #