mirror of
https://github.com/eworm-de/routeros-scripts
synced 2024-05-14 08:04:19 +00:00
1119 lines
35 KiB
Text
1119 lines
35 KiB
Text
#!rsc by RouterOS
|
|
# RouterOS script: global-functions
|
|
# Copyright (c) 2013-2021 Christian Hesse <mail@eworm.de>
|
|
# Michael Gisbers <michael@gisbers.de>
|
|
# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
|
|
#
|
|
# global functions
|
|
# https://git.eworm.de/cgit/routeros-scripts/about/
|
|
|
|
# expected configuration version
|
|
:global ExpectedConfigVersion 47;
|
|
|
|
# global variables not to be changed by user
|
|
:global GlobalFunctionsReady false;
|
|
:global Identity [ / system identity get name ];
|
|
|
|
# global functions
|
|
:global CertificateAvailable;
|
|
:global CertificateDownload;
|
|
:global CertificateNameByCN;
|
|
:global CharacterReplace;
|
|
:global CleanFilePath;
|
|
:global DefaultRouteIsReachable;
|
|
:global DeviceInfo;
|
|
:global DNSIsResolving;
|
|
:global DownloadPackage;
|
|
:global FlushEmailQueue;
|
|
:global FlushTelegramQueue;
|
|
:global GetMacVendor;
|
|
:global GetRandom20CharHex;
|
|
:global GetRandomNumber;
|
|
:global IfThenElse;
|
|
:global IPCalc;
|
|
:global LogPrintExit;
|
|
:global LogPrintExit2;
|
|
:global MkDir;
|
|
:global ParseKeyValueStore;
|
|
:global RandomDelay;
|
|
:global RequiredRouterOS;
|
|
:global ScriptFromTerminal;
|
|
:global ScriptInstallUpdate;
|
|
:global ScriptLock;
|
|
:global SendEMail;
|
|
:global SendNotification;
|
|
:global SendTelegram;
|
|
:global SymbolByUnicodeName;
|
|
:global SymbolForNotification;
|
|
:global TimeIsSync;
|
|
:global UrlEncode;
|
|
:global VersionToNum;
|
|
:global WaitDefaultRouteReachable;
|
|
:global WaitDNSResolving;
|
|
:global WaitForFile;
|
|
:global WaitFullyConnected;
|
|
:global WaitTimeSync;
|
|
|
|
# check and download required certificate
|
|
:set CertificateAvailable do={
|
|
:local CommonName [ :tostr $1 ];
|
|
|
|
:global CertificateDownload;
|
|
:global LogPrintExit2;
|
|
:global ParseKeyValueStore;
|
|
:global RequiredRouterOS;
|
|
|
|
:if ([ / system resource get free-hdd-space ] < 8388608 && \
|
|
[ / certificate settings get crl-download ] = true && \
|
|
[ / certificate settings get crl-store ] = "system") do={
|
|
$LogPrintExit2 warning $0 ("This system has low free flash space but " . \
|
|
"is configured to download certificate CRLs to system!") false;
|
|
}
|
|
|
|
:if ([ :len [ / certificate find where common-name=$CommonName ] ] = 0) do={
|
|
$LogPrintExit2 info $0 ("Certificate with CommonName \"" . $CommonName . "\" not available.") false;
|
|
:if ([ $CertificateDownload $CommonName ] = false) do={
|
|
:return false;
|
|
}
|
|
}
|
|
|
|
:if ([ $RequiredRouterOS $0 "6.47" ] = false) do={
|
|
:return true;
|
|
}
|
|
|
|
:local CertVal [ / certificate get [ find where common-name=$CommonName ] ];
|
|
:do {
|
|
:if ([ :len [ / certificate find where skid=($CertVal->"akid") ] ] = 0) do={
|
|
$LogPrintExit2 info $0 ("Certificate chain for \"" . $CommonName . \
|
|
"\" is incomplete, missing \"" . ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN") . "\".") false;
|
|
:if ([ $CertificateDownload $CommonName ] = false) do={
|
|
:return false;
|
|
}
|
|
}
|
|
:set CertVal [ / certificate get [ find where skid=($CertVal->"akid") ] ];
|
|
} while=(($CertVal->"akid") != "" && ($CertVal->"akid") != ($CertVal->"skid"));
|
|
:return true;
|
|
}
|
|
|
|
# download and import certificate
|
|
:set CertificateDownload do={
|
|
:local CommonName [ :tostr $1 ];
|
|
|
|
:global ScriptUpdatesBaseUrl;
|
|
:global ScriptUpdatesUrlSuffix;
|
|
|
|
:global CertificateNameByCN;
|
|
:global LogPrintExit2;
|
|
:global UrlEncode;
|
|
:global WaitForFile;
|
|
|
|
$LogPrintExit2 info $0 ("Downloading and importing certificate with " . \
|
|
"CommonName \"" . $CommonName . "\".") false;
|
|
:do {
|
|
:local LocalFileName ($CommonName . ".pem");
|
|
:local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
|
|
/ tool fetch check-certificate=yes-without-crl \
|
|
($ScriptUpdatesBaseUrl . "certs/" . \
|
|
$UrlFileName . $ScriptUpdatesUrlSuffix) \
|
|
dst-path=$LocalFileName as-value;
|
|
$WaitForFile $LocalFileName;
|
|
/ certificate import file-name=$LocalFileName passphrase="";
|
|
/ file remove $LocalFileName;
|
|
|
|
:foreach Cert in=[ / certificate find where name~("^" . $LocalFileName . "_[0-9]+\$") ] do={
|
|
$CertificateNameByCN [ / certificate get $Cert common-name ];
|
|
}
|
|
} on-error={
|
|
$LogPrintExit2 warning $0 ("Failed importing certificate with " . \
|
|
"CommonName \"" . $CommonName . "\"!") false;
|
|
:return false;
|
|
}
|
|
:return true;
|
|
}
|
|
|
|
# name a certificate by its common-name
|
|
:set CertificateNameByCN do={
|
|
:local CommonName [ :tostr $1 ];
|
|
|
|
:global CharacterReplace;
|
|
|
|
:local Cert [ / certificate find where common-name=$CommonName ];
|
|
/ certificate set $Cert \
|
|
name=[ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $CommonName "'" "-" ] " " "-" ] "---" "-" ];
|
|
}
|
|
|
|
# character replace
|
|
:set CharacterReplace do={
|
|
:local String [ :tostr $1 ];
|
|
:local ReplaceFrom [ :tostr $2 ];
|
|
:local ReplaceWith [ :tostr $3 ];
|
|
:local Return "";
|
|
|
|
:if ($ReplaceFrom = "") do={
|
|
:return $String;
|
|
}
|
|
|
|
:while ([ :typeof [ :find $String $ReplaceFrom ] ] != "nil") do={
|
|
:local Pos [ :find $String $ReplaceFrom ];
|
|
:set Return ($Return . [ :pick $String 0 $Pos ] . $ReplaceWith);
|
|
:set String [ :pick $String ($Pos + [ :len $ReplaceFrom ]) [ :len $String ] ];
|
|
}
|
|
|
|
:return ($Return . $String);
|
|
}
|
|
|
|
# clean file path
|
|
:set CleanFilePath do={
|
|
:local Path [ :tostr $1 ];
|
|
|
|
:global CharacterReplace;
|
|
|
|
:while ($Path ~ "//") do={
|
|
:set $Path [ $CharacterReplace $Path "//" "/" ];
|
|
}
|
|
:if ([ :pick $Path 0 ] = "/") do={
|
|
:set Path [ :pick $Path 1 [ :len $Path ] ];
|
|
}
|
|
:if ([ :pick $Path ([ :len $Path ] - 1) ] = "/") do={
|
|
:set Path [ :pick $Path 0 ([ :len $Path ] - 1) ];
|
|
}
|
|
|
|
:return $Path;
|
|
}
|
|
|
|
# default route is reachable
|
|
:set DefaultRouteIsReachable do={
|
|
:if ([ :len [ / ip route find where dst-address=0.0.0.0/0 active !blackhole !routing-mark !unreachable ] ] > 0) do={
|
|
:return true;
|
|
}
|
|
:return false;
|
|
}
|
|
|
|
# get readable device info
|
|
:set DeviceInfo do={
|
|
:global ExpectedConfigVersion;
|
|
:global GlobalConfigVersion;
|
|
:global Identity;
|
|
|
|
:global IfThenElse;
|
|
|
|
:local Resource [ / system resource get ];
|
|
:local RouterBoard [ / system routerboard get ];
|
|
:local Update [ / system package update get ];
|
|
|
|
:return ( \
|
|
"Hostname: " . $Identity . \
|
|
"\nBoard name: " . $Resource->"board-name" . \
|
|
"\nArchitecture: " . $Resource->"architecture-name" . \
|
|
[ $IfThenElse ($RouterBoard->"routerboard" = true) \
|
|
("\nModel: " . $RouterBoard->"model" . \
|
|
[ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
|
|
(" " . $RouterBoard->"revision") ] . \
|
|
"\nSerial number: " . $RouterBoard->"serial-number") ] . \
|
|
"\nRouterOS:" . \
|
|
"\n Channel: " . $Update->"channel" . \
|
|
"\n Installed: " . $Update->"installed-version" . \
|
|
[ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \
|
|
$Update->"installed-version" != $Update->"latest-version") \
|
|
("\n Available: " . $Update->"latest-version") ] . \
|
|
"\nRouterOS-Scripts:" . \
|
|
"\n Current: " . $GlobalConfigVersion . \
|
|
[ $IfThenElse ($GlobalConfigVersion != $ExpectedConfigVersion) \
|
|
("\n Expected: " . $ExpectedConfigVersion) ]);
|
|
}
|
|
|
|
# check if DNS is resolving
|
|
:set DNSIsResolving do={
|
|
:global CharacterReplace;
|
|
|
|
:do {
|
|
:resolve "low-ttl.eworm.de";
|
|
:return true;
|
|
} on-error={
|
|
:return false;
|
|
}
|
|
}
|
|
|
|
# download package from upgrade server
|
|
:set DownloadPackage do={
|
|
:local PkgName [ :tostr $1 ];
|
|
:local PkgVer [ :tostr $2 ];
|
|
:local PkgArch [ :tostr $3 ];
|
|
:local PkgDir [ :tostr $4 ];
|
|
|
|
:global CertificateAvailable;
|
|
:global CleanFilePath;
|
|
:global LogPrintExit2;
|
|
:global 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 ]; }
|
|
|
|
:local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk");
|
|
:if ($PkgArch = "x86_64" || $PkgName ~ "^routeros-") do={
|
|
:set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
|
|
}
|
|
:local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];
|
|
|
|
:if ([ :len [ / file find where name=$PkgDest type="package" ] ] > 0) do={
|
|
$LogPrintExit2 info $0 ("Package file alreasy exists.") false;
|
|
:return true;
|
|
}
|
|
|
|
:if ([ $CertificateAvailable "R3" ] = false) do={
|
|
$LogPrintExit2 error $0 ("Downloading required certificate failed.") true;
|
|
}
|
|
|
|
:local Retry 3;
|
|
:while ($Retry > 0) do={
|
|
:do {
|
|
/ tool fetch check-certificate=yes-without-crl \
|
|
("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile) \
|
|
dst-path=$PkgDest;
|
|
$WaitForFile $PkgDest;
|
|
|
|
:if ([ / file get [ find where name=$PkgDest ] type ] = "package") do={
|
|
:return true;
|
|
}
|
|
} on-error={
|
|
$LogPrintExit2 debug $0 ("Downloading package failed.") false;
|
|
}
|
|
|
|
/ file remove [ find where name=$PkgDest ];
|
|
:set Retry ($Retry - 1);
|
|
}
|
|
|
|
:return false;
|
|
}
|
|
|
|
# flush e-mail queue
|
|
:set FlushEmailQueue do={
|
|
:global EmailQueue;
|
|
|
|
:global LogPrintExit2;
|
|
|
|
:local AllDone true;
|
|
:local QueueLen [ :len $EmailQueue ];
|
|
|
|
:if ([ :len [ / system scheduler find where name="FlushEmailQueue" ] ] > 0 && $QueueLen = 0) do={
|
|
$LogPrintExit2 warning $0 ("Flushing E-Mail messages from scheduler, but queue is empty.") false;
|
|
}
|
|
|
|
/ system scheduler set interval=1m [ find where name="FlushEmailQueue" interval=1s ];
|
|
|
|
:foreach Id,Message in=$EmailQueue do={
|
|
:if ([ :typeof $Message ] = "array" ) do={
|
|
/ tool e-mail send to=($Message->"to") cc=($Message->"cc") \
|
|
subject=($Message->"subject") body=($Message->"body");
|
|
:local Wait true;
|
|
:do {
|
|
:delay 1s;
|
|
:local Status [ / tool e-mail get last-status ];
|
|
:if ($Status = "succeeded") do={
|
|
:set ($EmailQueue->$Id);
|
|
:set Wait false;
|
|
}
|
|
:if ($Status = "failed") do={
|
|
:set AllDone false;
|
|
:set Wait false;
|
|
}
|
|
} while ($Wait = true);
|
|
}
|
|
}
|
|
|
|
:if ($AllDone = true && $QueueLen = [ :len $EmailQueue ]) do={
|
|
/ system scheduler remove [ find where name="FlushEmailQueue" ];
|
|
:set EmailQueue;
|
|
}
|
|
}
|
|
|
|
# flush telegram queue
|
|
:set FlushTelegramQueue do={
|
|
:global TelegramQueue;
|
|
:global TelegramTokenId;
|
|
|
|
:global LogPrintExit2;
|
|
|
|
:local AllDone true;
|
|
:local QueueLen [ :len $TelegramQueue ];
|
|
|
|
:if ([ :len [ / system scheduler find where name="FlushTelegramQueue" ] ] > 0 && $QueueLen = 0) do={
|
|
$LogPrintExit2 warning $0 ("Flushing Telegram messages from scheduler, but queue is empty.") false;
|
|
}
|
|
|
|
:foreach Id,Message in=$TelegramQueue do={
|
|
:if ([ :typeof $Message ] = "array" ) do={
|
|
:do {
|
|
/ tool fetch check-certificate=yes-without-crl output=none http-method=post \
|
|
("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
|
|
http-data=("chat_id=" . ($Message->"chatid") . \
|
|
"&disable_notification=" . ($Message->"silent") . \
|
|
"&disable_web_page_preview=true&parse_mode=" . ($Message->"parsemode") . \
|
|
"&text=" . ($Message->"text")) as-value;
|
|
:set ($TelegramQueue->$Id);
|
|
} on-error={
|
|
$LogPrintExit2 debug $0 ("Sending queued Telegram message failed.") false;
|
|
:set AllDone false;
|
|
}
|
|
}
|
|
}
|
|
|
|
:if ($AllDone = true && $QueueLen = [ :len $TelegramQueue ]) do={
|
|
/ system scheduler remove [ find where name="FlushTelegramQueue" ];
|
|
:set TelegramQueue;
|
|
}
|
|
}
|
|
|
|
# get MAC vendor
|
|
:set GetMacVendor do={
|
|
:local Mac [ :tostr $1 ];
|
|
|
|
:global CertificateAvailable;
|
|
:global LogPrintExit2;
|
|
|
|
:do {
|
|
:if ([ $CertificateAvailable "Cloudflare Inc ECC CA-3" ] = false) do={
|
|
$LogPrintExit2 warning $0 ("Downloading required certificate failed.") true;
|
|
}
|
|
:local Vendor ([ / tool fetch check-certificate=yes-without-crl \
|
|
("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data");
|
|
:return $Vendor;
|
|
} on-error={
|
|
:do {
|
|
/ tool fetch check-certificate=yes-without-crl ("https://api.macvendors.com/") \
|
|
output=none as-value;
|
|
$LogPrintExit2 debug $0 ("The mac vendor is not known in database.") false;
|
|
} on-error={
|
|
$LogPrintExit2 warning $0 ("Failed getting mac vendor.") false;
|
|
}
|
|
:return "unknown vendor";
|
|
}
|
|
}
|
|
|
|
# generate random 20 chars hex (0-9 and a-f)
|
|
:set GetRandom20CharHex do={
|
|
:local Random ([ / certificate scep-server otp generate minutes-valid=0 as-value ]->"password");
|
|
/ certificate scep-server otp remove [ find where password=$Random ];
|
|
:return $Random;
|
|
}
|
|
|
|
# generate random number
|
|
:set GetRandomNumber do={
|
|
:local Max 4294967295;
|
|
:if ([ :typeof $1 ] != "nothing" ) do={
|
|
:set Max ([ :tonum $1 ] + 1);
|
|
}
|
|
|
|
:global GetRandom20CharHex;
|
|
|
|
:local Num;
|
|
:local 40CharHex ([ $GetRandom20CharHex ] . [ $GetRandom20CharHex ]);
|
|
|
|
:for I from=0 to=39 do={
|
|
:local Char [ :pick $40CharHex $I ];
|
|
:if ($Char~"[0-9]") do={
|
|
:set Num ($Num . $Char);
|
|
}
|
|
}
|
|
|
|
:return ([ :tonum [ :pick $Num 0 18 ] ] % $Max);
|
|
}
|
|
|
|
# mimic conditional/ternary operator (condition ? consequent : alternative)
|
|
:set IfThenElse do={
|
|
:if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={
|
|
:return $2;
|
|
}
|
|
:return $3;
|
|
}
|
|
|
|
# calculate and print netmask, network, min host, max host and broadcast
|
|
:set IPCalc do={
|
|
:local Input [ :tostr $1 ];
|
|
:local Address [ :toip [ :pick $Input 0 [ :find $Input "/" ] ] ];
|
|
:local Bits [ :tonum [ :pick $Input ([ :find $Input "/" ] + 1) [ :len $Input ] ] ];
|
|
:local Mask ((255.255.255.255 << (32 - $Bits)) & 255.255.255.255);
|
|
|
|
:local Return {
|
|
"address"=$Address;
|
|
"netmask"=$Mask;
|
|
"networkaddress"=($Address & $Mask);
|
|
"networkbits"=$Bits;
|
|
"network"=(($Address & $Mask) . "/" . $Bits);
|
|
"hostmin"=(($Address & $Mask) | 0.0.0.1);
|
|
"hostmax"=(($Address | ~$Mask) ^ 0.0.0.1);
|
|
"broadcast"=($Address | ~$Mask);
|
|
}
|
|
|
|
:put ( \
|
|
"Address: " . $Return->"address" . "\n\r" . \
|
|
"Netmask: " . $Return->"netmask" . "\n\r" . \
|
|
"Network: " . $Return->"network" . "\n\r" . \
|
|
"HostMin: " . $Return->"hostmin" . "\n\r" . \
|
|
"HostMax: " . $Return->"hostmax" . "\n\r" . \
|
|
"Broadcast: " . $Return->"broadcast");
|
|
|
|
:return $Return;
|
|
}
|
|
|
|
# deprecated compatibility wrapper
|
|
:set LogPrintExit do={
|
|
:global LogPrintExit2;
|
|
|
|
$LogPrintExit2 $1 "unknown" $2 $3;
|
|
}
|
|
|
|
# log and print with same text, optionally exit
|
|
:set LogPrintExit2 do={
|
|
:local Severity [ :tostr $1 ];
|
|
:local Name [ :tostr $2 ];
|
|
:local Message [ :tostr $3 ];
|
|
:local Exit [ :tostr $4 ];
|
|
|
|
:global PrintDebug;
|
|
|
|
: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 ($Name . ": " . $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" || $PrintDebug = true) do={
|
|
:if ($Exit = "true") do={
|
|
:error ([ $PrintSeverity $Severity ] . ": " . $Message);
|
|
} else={
|
|
:put ([ $PrintSeverity $Severity ] . ": " . $Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
# create directory
|
|
:set MkDir do={
|
|
:local Dir [ :tostr $1 ];
|
|
|
|
:global CleanFilePath;
|
|
:global WaitForFile;
|
|
|
|
:set Dir [ $CleanFilePath $Dir ];
|
|
|
|
:if ([ :len [ / file find where name=$Dir type="directory" ] ] = 1) do={
|
|
:return true;
|
|
}
|
|
|
|
:local Return true;
|
|
:local WwwVal [ / ip service get www ];
|
|
/ ip service set www address=127.0.0.1/32 disabled=no port=80;
|
|
:do {
|
|
/ tool fetch http://127.0.0.1/ dst-path=($Dir . "/tmp") as-value;
|
|
$WaitForFile ($Dir . "/tmp");
|
|
/ file remove ($Dir . "/tmp");
|
|
} on-error={
|
|
:set Return false;
|
|
}
|
|
/ ip service set www address=($WwwVal->"address") \
|
|
disabled=($WwwVal->"disabled") port=($WwwVal->"port");
|
|
:return $Return;
|
|
}
|
|
|
|
# parse key value store
|
|
:set ParseKeyValueStore do={
|
|
:local Source $1;
|
|
:if ([ :typeof $Source ] != "array") do={
|
|
:set Source [ :tostr $1 ];
|
|
}
|
|
:local Result [ :toarray "" ];
|
|
:foreach KeyValue in=[ :toarray $Source ] do={
|
|
:if ([ :find $KeyValue "=" ]) do={
|
|
:set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \
|
|
[ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
|
|
} else={
|
|
:set ($Result->$KeyValue) true;
|
|
}
|
|
}
|
|
:return $Result;
|
|
}
|
|
|
|
# delay a random amount of seconds
|
|
:set RandomDelay do={
|
|
:global GetRandomNumber;
|
|
|
|
:delay ([ $GetRandomNumber $1 ] . "s");
|
|
}
|
|
|
|
# check for required RouterOS version
|
|
:set RequiredRouterOS do={
|
|
:local Caller [ :tostr $1 ];
|
|
:local Required [ :tostr $2 ];
|
|
|
|
:global IfThenElse;
|
|
:global LogPrintExit2;
|
|
:global VersionToNum;
|
|
|
|
:if ([ $VersionToNum $Required ] > [ $VersionToNum [ / system package update get installed-version ] ]) do={
|
|
$LogPrintExit2 warning $0 ("This " . [ $IfThenElse ([ :pick $Caller 0 ] = "\$") "function" "script" ] . \
|
|
" '" . $Caller . "' (at least specific functionality) requires RouterOS " . $Required . ". Please update!") false;
|
|
:return false;
|
|
}
|
|
:return true;
|
|
}
|
|
|
|
# check if script is run from terminal
|
|
:set ScriptFromTerminal do={
|
|
:local Script [ :tostr $1 ];
|
|
|
|
:global LogPrintExit2;
|
|
|
|
:foreach Job in=[ / system script job find where script=$Script ] do={
|
|
:set Job [ / system script job get $Job ];
|
|
:while ([ :typeof ($Job->"parent") ] = "id") do={
|
|
:set Job [ / system script job get [ find where .id=($Job->"parent") ] ];
|
|
}
|
|
:if (($Job->"type") = "login") do={
|
|
$LogPrintExit2 debug $0 ("Script " . $Script . " started from terminal.") false;
|
|
:return true;
|
|
}
|
|
}
|
|
$LogPrintExit2 debug $0 ("Script " . $Script . " NOT started from terminal.") false;
|
|
|
|
:return false;
|
|
}
|
|
|
|
# install new scripts, update existing scripts
|
|
:set ScriptInstallUpdate do={
|
|
:local Scripts [ :toarray $1 ];
|
|
|
|
:global ExpectedConfigVersion;
|
|
:global GlobalConfigVersion;
|
|
:global Identity;
|
|
:global IDonate;
|
|
:global NotificationsWithSymbols;
|
|
:global ScriptUpdatesBaseUrl;
|
|
:global ScriptUpdatesFetch;
|
|
:global ScriptUpdatesUrlSuffix;
|
|
:global SentConfigChangesNotification;
|
|
|
|
:global CertificateAvailable;
|
|
:global IfThenElse;
|
|
:global LogPrintExit2;
|
|
:global ParseKeyValueStore;
|
|
:global ScriptInstallUpdate;
|
|
:global SendNotification;
|
|
:global SymbolForNotification;
|
|
|
|
:if ([ $CertificateAvailable "R3" ] = false) do={
|
|
$LogPrintExit2 warning $0 ("Downloading certificate failed, trying without.") false;
|
|
}
|
|
|
|
:foreach Script in=$Scripts do={
|
|
:if ([ :len [ / system script find where name=$Script ] ] = 0) do={
|
|
$LogPrintExit2 info $0 ("Adding new script: " . $Script) false;
|
|
/ system script add name=$Script source="#!rsc by RouterOS\n";
|
|
}
|
|
}
|
|
|
|
:local ScriptInstallUpdateBefore [ :tostr $ScriptInstallUpdate ];
|
|
|
|
:foreach Script in=[ / system script find where source~"^#!rsc( by RouterOS)\?\n" ] do={
|
|
:local ScriptVal [ / system script get $Script ];
|
|
:local ScriptFile [ / file find where name=("script-updates/" . $ScriptVal->"name") ];
|
|
:local SourceNew;
|
|
:if ([ :len $ScriptFile ] > 0) do={
|
|
:set SourceNew [ / file get $ScriptFile content ];
|
|
/ file remove $ScriptFile;
|
|
}
|
|
|
|
:foreach Scheduler in=[ / system scheduler find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={
|
|
:local SchedulerVal [ / system scheduler get $Scheduler ];
|
|
:if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={
|
|
$LogPrintExit2 warning $0 ("Policies differ for script " . $ScriptVal->"name" . \
|
|
" and its scheduler " . $SchedulerVal->"name" . "!") false;
|
|
}
|
|
}
|
|
|
|
:if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
|
|
:local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ];
|
|
:if (!($Comment->"ignore" = true)) do={
|
|
$LogPrintExit2 debug $0 ("Fetching script from url: " . $ScriptVal->"name") false;
|
|
:do {
|
|
:local BaseUrl $ScriptUpdatesBaseUrl;
|
|
:local UrlSuffix $ScriptUpdatesUrlSuffix;
|
|
:if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); }
|
|
:if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); }
|
|
|
|
:local Result [ / tool fetch check-certificate=yes-without-crl \
|
|
($BaseUrl . $ScriptVal->"name" . $UrlSuffix) output=user as-value ];
|
|
:if ($Result->"status" = "finished") do={
|
|
:set SourceNew ($Result->"data");
|
|
}
|
|
} on-error={
|
|
$LogPrintExit2 warning $0 ("Failed fetching " . $ScriptVal->"name") false;
|
|
}
|
|
}
|
|
}
|
|
|
|
:if ([ :len $SourceNew ] > 0) do={
|
|
:if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
|
|
:if ($SourceNew != $ScriptVal->"source") do={
|
|
:local DontRequirePermissions \
|
|
($SourceNew~"\n# requires: dont-require-permissions=yes\n");
|
|
$LogPrintExit2 info $0 ("Updating script: " . $ScriptVal->"name") false;
|
|
/ system script set owner=($ScriptVal->"name") source=$SourceNew \
|
|
dont-require-permissions=$DontRequirePermissions $Script;
|
|
:if ($ScriptVal->"name" = "global-config") do={
|
|
$LogPrintExit2 info $0 ("Reloading global configuration and overlay.") false;
|
|
:do {
|
|
/ system script { run global-config; run global-config-overlay; }
|
|
} on-error={
|
|
$LogPrintExit2 error $0 ("Reloading global configuration and overlay failed! Syntax error\?") false;
|
|
}
|
|
}
|
|
:if ($ScriptVal->"name" = "global-functions") do={
|
|
$LogPrintExit2 info $0 ("Reloading global functions.") false;
|
|
:do {
|
|
/ system script run global-functions;
|
|
} on-error={
|
|
$LogPrintExit2 error $0 ("Reloading global functions failed!") false;
|
|
}
|
|
}
|
|
} else={
|
|
$LogPrintExit2 debug $0 ("Script " . $ScriptVal->"name" . " did not change.") false;
|
|
}
|
|
} else={
|
|
$LogPrintExit2 warning $0 ("Looks like new script " . $ScriptVal->"name" . " is not valid. Ignoring!") false;
|
|
}
|
|
} else={
|
|
$LogPrintExit2 debug $0 ("No update for script " . $ScriptVal->"name" . ".") false;
|
|
}
|
|
}
|
|
|
|
:if ($SentConfigChangesNotification!=$ExpectedConfigVersion && \
|
|
$GlobalConfigVersion < $ExpectedConfigVersion) do={
|
|
:global GlobalConfigChanges;
|
|
:global GlobalConfigMigration;
|
|
:local ChangeLogCode;
|
|
:local NotificationMessage ("Current configuration on " . $Identity . \
|
|
" is out of date. Please update global-config-overlay, then increase " . \
|
|
"\$GlobalConfigVersion (currently " . $GlobalConfigVersion . \
|
|
") to " . $ExpectedConfigVersion . " and re-run global-config-overlay.");
|
|
$LogPrintExit2 info $0 ($NotificationMessage) false;
|
|
|
|
$LogPrintExit2 debug $0 ("Fetching changelog.") false;
|
|
:do {
|
|
:local Result [ / tool fetch check-certificate=yes-without-crl \
|
|
($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix) \
|
|
output=user as-value ];
|
|
:if ($Result->"status" = "finished") do={
|
|
:set ChangeLogCode ($Result->"data");
|
|
}
|
|
:set NotificationMessage ($NotificationMessage . "\n\nChanges:");
|
|
[ :parse $ChangeLogCode ];
|
|
:for I from=($GlobalConfigVersion + 1) to=$ExpectedConfigVersion do={
|
|
:local Migration ($GlobalConfigMigration->[ :tostr $I ]);
|
|
:if ([ :typeof $Migration ] = "str") do={
|
|
$LogPrintExit2 info $0 ("Applying migration: " . $Migration) false;
|
|
[ :parse $Migration ];
|
|
}
|
|
:set NotificationMessage ($NotificationMessage . \
|
|
"\n " . [ $IfThenElse ($NotificationsWithSymbols = true) ("\E2\97\8F") "*" ] . " " . \
|
|
$GlobalConfigChanges->[ :tostr $I ]);
|
|
$LogPrintExit2 info $0 ("Change: " . $GlobalConfigChanges->[ :tostr $I ]) false;
|
|
}
|
|
:set GlobalConfigChanges;
|
|
:set GlobalConfigMigration;
|
|
} on-error={
|
|
$LogPrintExit2 warning $0 ("Failed fetching changes!") false;
|
|
:set NotificationMessage ($NotificationMessage . \
|
|
"\n\nChanges are not available.");
|
|
}
|
|
|
|
:local Link;
|
|
:if ($IDonate != true) do={
|
|
:set NotificationMessage ($NotificationMessage . \
|
|
"\n\n==== donation hint ====\n" . \
|
|
"This project is developed in private spare time and usage is " . \
|
|
"free of charge for you. If you like the scripts and think this is " . \
|
|
"of value for you or your business please consider a donation.");
|
|
:set Link "https://git.eworm.de/cgit/routeros-scripts/about/#donate";
|
|
}
|
|
|
|
$SendNotification ([ $SymbolForNotification "pushpin" ] . "News and configuration changes") \
|
|
$NotificationMessage $Link;
|
|
:set SentConfigChangesNotification $ExpectedConfigVersion;
|
|
}
|
|
|
|
:if ($ScriptInstallUpdateBefore != [ :tostr $ScriptInstallUpdate ]) do={
|
|
$LogPrintExit2 info $0 ("This function changed, you may want to re-run.") false;
|
|
}
|
|
}
|
|
|
|
# lock script against multiple invocation
|
|
:set ScriptLock do={
|
|
:global LogPrintExit2;
|
|
|
|
:local Script [ :tostr $1 ];
|
|
|
|
:if ([ :len [ / system script job find where script=$Script ] ] > 1) do={
|
|
$LogPrintExit2 info $0 ("Script " . $Script . " started more than once... Aborting.") true;
|
|
}
|
|
}
|
|
|
|
# send notification via e-mail
|
|
:set SendEMail do={
|
|
:local Subject [ :tostr $1 ];
|
|
:local Message [ :tostr $2 ];
|
|
:local Link [ :tostr $3 ];
|
|
|
|
:global Identity;
|
|
:global EmailGeneralTo;
|
|
:global EmailGeneralCc;
|
|
:global EmailQueue;
|
|
|
|
:global LogPrintExit2;
|
|
:global IfThenElse;
|
|
|
|
:if ([ :len $EmailGeneralTo ] = 0) do={
|
|
:return false;
|
|
}
|
|
|
|
:if ([ :typeof $EmailQueue ] = "nothing") do={
|
|
:set EmailQueue [ :toarray "" ];
|
|
}
|
|
:local Signature [ / system note get note ];
|
|
:set ($EmailQueue->[ :len $EmailQueue ]) {
|
|
to=$EmailGeneralTo; cc=$EmailGeneralCc; subject=("[" . $Identity . "] " . $Subject);
|
|
body=($Message . \
|
|
[ $IfThenElse ([ :len $Link ] > 0) ("\n\n" . $Link) "" ] . \
|
|
[ $IfThenElse ([ :len $Signature ] > 0) ("\n-- \n" . $Signature) "" ]) };
|
|
:if ([ :len [ / system scheduler find where name="FlushEmailQueue" ] ] = 0) do={
|
|
/ system scheduler add name=FlushEmailQueue interval=1s start-time=startup \
|
|
on-event=":global FlushEmailQueue; \$FlushEmailQueue;";
|
|
}
|
|
}
|
|
|
|
# send notification via e-mail and telegram
|
|
# Note that attachment is ignored for telegram, silent is ignored for e-mail!
|
|
:set SendNotification do={
|
|
:local Subject [ :tostr $1 ];
|
|
:local Message [ :tostr $2 ];
|
|
:local Link [ :tostr $3 ];
|
|
:local Silent [ :tostr $4 ];
|
|
|
|
:global SendEMail;
|
|
:global SendTelegram;
|
|
|
|
$SendEMail $Subject $Message $Link;
|
|
$SendTelegram $Subject $Message $Link $Silent;
|
|
}
|
|
|
|
# send notification via telegram
|
|
:set SendTelegram do={
|
|
:local Subject [ :tostr $1 ];
|
|
:local Message [ :tostr $2 ];
|
|
:local Link [ :tostr $3 ];
|
|
:local Silent [ :tostr $4 ];
|
|
|
|
:global Identity;
|
|
:global TelegramChatId;
|
|
:global TelegramChatIdOverride;
|
|
:global TelegramFixedWidthFont;
|
|
:global TelegramQueue;
|
|
:global TelegramTokenId;
|
|
|
|
:global CertificateAvailable;
|
|
:global CharacterReplace;
|
|
:global IfThenElse;
|
|
:global LogPrintExit2;
|
|
:global SymbolForNotification;
|
|
:global UrlEncode;
|
|
|
|
:local EscapeMD do={
|
|
:global TelegramFixedWidthFont;
|
|
|
|
:global CharacterReplace;
|
|
:global IfThenElse;
|
|
|
|
:if ($TelegramFixedWidthFont != true) do={
|
|
:return ($1 . [ $IfThenElse ($2 = "body") "\n" "" ]);
|
|
}
|
|
|
|
:local Return $1;
|
|
:local Chars {
|
|
"body"={ "\\"; "`" };
|
|
"hint"={ "_"; "*"; "["; "]"; "("; ")"; "~"; "`"; ">";
|
|
"#"; "+"; "-"; "="; "|"; "{"; "}"; "."; "!" };
|
|
}
|
|
:foreach Char in=($Chars->$2) do={
|
|
:set Return [ $CharacterReplace $Return $Char ("\\" . $Char) ];
|
|
}
|
|
|
|
:if ($2 = "body") do={
|
|
:return ("```\n" . $Return . "\n```");
|
|
}
|
|
|
|
:return $Return;
|
|
}
|
|
|
|
:local ChatId $TelegramChatId;
|
|
:if ([ :len $TelegramChatIdOverride ] > 0) do={
|
|
:set ChatId $TelegramChatIdOverride;
|
|
}
|
|
|
|
:if ([ :len $TelegramTokenId ] = 0 || [ :len $ChatId ] = 0) do={
|
|
:return false;
|
|
}
|
|
|
|
:local Truncated false;
|
|
:local LenLink [ :len $Link ];
|
|
:local Text ("[" . $Identity . "] " . $Subject . "\n\n" . $Message);
|
|
:local LenText [ :len $Text ];
|
|
:if ($LenText > (3968 - $LenLink)) do={
|
|
:set Text [ $EscapeMD ([ :pick $Text 0 (3840 - $LenLink) ] . "...") "body" ];
|
|
:set Truncated true;
|
|
} else={
|
|
:set Text [ $EscapeMD $Text "body" ];
|
|
}
|
|
:if ($LenLink > 0) do={
|
|
:set Text ($Text . "\n" . [ $SymbolForNotification "link" ] . [ $EscapeMD $Link "hint" ]);
|
|
}
|
|
:if ($Truncated = true) do={
|
|
:set Text ($Text . "\n" . [ $SymbolForNotification "scissors" ] . \
|
|
[ $EscapeMD ("The Telegram message was too long and has been truncated, cut off " . \
|
|
(($LenText - [ :len $Text ]) * 100 / $LenText) . "%!") "hint" ]);
|
|
}
|
|
:set Text [ $UrlEncode $Text ];
|
|
:local ParseMode [ $IfThenElse ($TelegramFixedWidthFont = true) "MarkdownV2" "" ];
|
|
|
|
:do {
|
|
:if ([ $CertificateAvailable "Go Daddy Secure Certificate Authority - G2" ] = false) do={
|
|
$LogPrintExit2 warning $0 ("Downloading required certificate failed.") true;
|
|
}
|
|
/ tool fetch check-certificate=yes-without-crl output=none http-method=post \
|
|
("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
|
|
http-data=("chat_id=" . $ChatId . "&disable_notification=" . $Silent . \
|
|
"&disable_web_page_preview=true&parse_mode=" . $ParseMode . "&text=" . $Text) as-value;
|
|
} on-error={
|
|
$LogPrintExit2 info $0 ("Failed sending telegram notification! Queuing...") false;
|
|
|
|
:if ([ :typeof $TelegramQueue ] = "nothing") do={
|
|
:set TelegramQueue [ :toarray "" ];
|
|
}
|
|
:set Text ($Text . [ $UrlEncode ("\n" . [ $SymbolForNotification "alarm-clock" ] . \
|
|
[ $EscapeMD ("This message was queued since " . [ / system clock get date ] . \
|
|
" " . [ / system clock get time ] . " and may be obsolete.") "hint" ]) ]);
|
|
:set ($TelegramQueue->[ :len $TelegramQueue ]) {
|
|
chatid=$ChatId; parsemode=$ParseMode; text=$Text; silent=$Silent };
|
|
:if ([ :len [ / system scheduler find where name="FlushTelegramQueue" ] ] = 0) do={
|
|
/ system scheduler add name=FlushTelegramQueue interval=1m start-time=startup \
|
|
on-event=":global FlushTelegramQueue; \$FlushTelegramQueue;";
|
|
}
|
|
}
|
|
}
|
|
|
|
# return UTF-8 symbol for unicode name
|
|
:set SymbolByUnicodeName do={
|
|
:local Symbols {
|
|
"alarm-clock"="\E2\8F\B0";
|
|
"calendar"="\F0\9F\93\85";
|
|
"cross-mark"="\E2\9D\8C";
|
|
"fire"="\F0\9F\94\A5";
|
|
"floppy-disk"="\F0\9F\92\BE";
|
|
"high-voltage-sign"="\E2\9A\A1";
|
|
"incoming-envelope"="\F0\9F\93\A8";
|
|
"link"="\F0\9F\94\97";
|
|
"lock-with-ink-pen"="\F0\9F\94\8F";
|
|
"mobile-phone"="\F0\9F\93\B1";
|
|
"pushpin"="\F0\9F\93\8C";
|
|
"scissors"="\E2\9C\82";
|
|
"sparkles"="\E2\9C\A8";
|
|
"warning-sign"="\E2\9A\A0";
|
|
"white-heavy-check-mark"="\E2\9C\85"
|
|
}
|
|
|
|
:return ($Symbols->$1);
|
|
}
|
|
|
|
# return symbol for notification
|
|
:set SymbolForNotification do={
|
|
:global NotificationsWithSymbols;
|
|
:global SymbolByUnicodeName;
|
|
|
|
:if ($NotificationsWithSymbols != true) do={
|
|
:return "";
|
|
}
|
|
:local Return "";
|
|
:foreach Symbol in=[ :toarray $1 ] do={
|
|
:set Return ($Return . [ $SymbolByUnicodeName $Symbol ]);
|
|
}
|
|
:return ($Return . " ");
|
|
}
|
|
|
|
# check if system time is sync
|
|
:set TimeIsSync do={
|
|
:global LogPrintExit2;
|
|
|
|
:if ([ / system ntp client get enabled ] = true) do={
|
|
:do {
|
|
:if ([ / system ntp client get status ] = "synchronized") do={
|
|
:return true;
|
|
}
|
|
} on-error={
|
|
:if ([ :typeof [ / system ntp client get last-adjustment ] ] = "time") do={
|
|
:return true;
|
|
}
|
|
}
|
|
:return false;
|
|
}
|
|
|
|
:if ([ / ip cloud get ddns-enabled ] = true && [ / ip cloud get update-time ] = true) do={
|
|
:if ([ :typeof [ / ip cloud get public-address ] ] = "ip") do={
|
|
:return true;
|
|
}
|
|
:return false;
|
|
}
|
|
|
|
$LogPrintExit2 debug $0 ("No time source configured! Returning gracefully...") false;
|
|
:return true;
|
|
}
|
|
|
|
# url encoding
|
|
:set UrlEncode do={
|
|
:local Input [ :tostr $1 ];
|
|
:local Return "";
|
|
|
|
:if ([ :len $Input ] > 0) do={
|
|
: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 ([ :len $Replace ] > 0) do={
|
|
:set Char ($Subs->$Replace);
|
|
}
|
|
:set Return ($Return . $Char);
|
|
}
|
|
}
|
|
|
|
:return $Return;
|
|
}
|
|
|
|
# convert version string to numeric value
|
|
:set VersionToNum do={
|
|
:local Input [ :tostr $1 ];
|
|
:local Multi 0x1000000;
|
|
:local Return 0;
|
|
|
|
:global CharacterReplace;
|
|
|
|
:set Input [ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $Input \
|
|
"." "," ] "beta" ",beta," ] "rc" ",rc," ];
|
|
|
|
:foreach Value in=([ :toarray $Input ], 0) do={
|
|
:local Num [ :tonum $Value ];
|
|
:if ($Multi = 0x100) do={
|
|
:if ([ :typeof $Num ] = "num") do={
|
|
:set Return ($Return + 0xff00);
|
|
:set Multi ($Multi / 0x100);
|
|
} else={
|
|
:if ($Value = "beta") do={ :set Return ($Return + 0x3f00); }
|
|
:if ($Value = "rc") do={ :set Return ($Return + 0x7f00); }
|
|
}
|
|
}
|
|
:if ([ :typeof $Num ] = "num") do={ :set Return ($Return + ($Value * $Multi)); }
|
|
:set Multi ($Multi / 0x100);
|
|
}
|
|
|
|
:return $Return;
|
|
}
|
|
|
|
# wait for default route to be reachable
|
|
:set WaitDefaultRouteReachable do={
|
|
:global DefaultRouteIsReachable;
|
|
|
|
:while ([ $DefaultRouteIsReachable ] = false) do={
|
|
:delay 1s;
|
|
}
|
|
}
|
|
|
|
# wait for DNS to resolve
|
|
:set WaitDNSResolving do={
|
|
:global DNSIsResolving;
|
|
|
|
:while ([ $DNSIsResolving ] = false) do={
|
|
:delay 1s;
|
|
}
|
|
}
|
|
|
|
# wait for file to be available
|
|
:set WaitForFile do={
|
|
:local FileName [ :tostr $1 ];
|
|
|
|
:global CleanFilePath;
|
|
|
|
:set FileName [ $CleanFilePath $FileName ];
|
|
:local I 0;
|
|
|
|
:while ([ :len [ / file find where name=$FileName ] ] = 0) do={
|
|
:if ($I > 20) do={
|
|
:return false;
|
|
}
|
|
:delay 100ms;
|
|
:set I ($I + 1);
|
|
}
|
|
:return true;
|
|
}
|
|
|
|
# wait to be fully connected (default route is reachable, time is sync, DNS resolves)
|
|
:set WaitFullyConnected do={
|
|
:global WaitDefaultRouteReachable;
|
|
:global WaitDNSResolving;
|
|
:global WaitTimeSync;
|
|
|
|
$WaitDefaultRouteReachable;
|
|
$WaitTimeSync;
|
|
$WaitDNSResolving;
|
|
}
|
|
|
|
# wait for time to become synced
|
|
:set WaitTimeSync do={
|
|
:global LogPrintExit2;
|
|
:global TimeIsSync;
|
|
|
|
:while ([ $TimeIsSync ] = false) do={
|
|
:if ([ :len [ / system script find where name="rotate-ntp" ] ] > 0 && \
|
|
([ / system resource get uptime ] % (180 * 1000000000)) = 0s) do={
|
|
:do {
|
|
/ system script run rotate-ntp;
|
|
} on-error={
|
|
$LogPrintExit2 debug $0 ("Running rotate-ntp failed.") false;
|
|
}
|
|
}
|
|
:delay 1s;
|
|
}
|
|
}
|
|
|
|
# check for required RouterOS version
|
|
$RequiredRouterOS "global-functions" "6.47";
|
|
|
|
# signal we are ready
|
|
:set GlobalFunctionsReady true;
|