routeros-scripts/global-functions
2020-02-24 10:48:04 +01:00

460 lines
13 KiB
Text

#!rsc
# RouterOS script: global-functions
# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
# Michael Gisbers <michael@gisbers.de>
#
# global functions
# expected configuration version
:global ExpectedConfigVersion 12;
# global variables not to be changed by user
:global SentConfigChangesNotification "-";
:global SentRouterosUpdateNotification "-";
:global SentLteFirmwareUpgradeNotification "-";
:global Identity [ / system identity get name ];
# global functions
:global UrlEncode;
:global CharacterReplace;
:global CertificateNameByCN;
:global CertificateDownload;
:global CertificateAvailable;
:global SendEMail;
:global SendTelegram;
:global SendNotification;
:global GetMacVendor;
:global CleanFilePath;
:global DownloadPackage;
:global ScriptLock;
:global ScriptFromTerminal;
:global WaitForFile;
:global ParseKeyValueStore;
:global GetRandom;
:global RandomDelay;
:global DeviceInfo;
:global ScriptInstallUpdate;
:global MailServerIsUp;
# url encoding
:set UrlEncode do={
:local Input [ :tostr $1 ];
:local Return "";
:if ([ :len $Input ] > 0) do={
:local Chars " !\"#\$%&'()*+,:;<=>\?@[\\]^`{|}~";
:local Subs { "%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;
}
# 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);
}
# 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 $CommonName " " "-" ] "---" "-" ];
}
# download and import certificate
:set CertificateDownload do={
:local CommonName [ :tostr $1 ];
:global ScriptUpdatesBaseUrl;
:global ScriptUpdatesUrlSuffix;
:global CertificateNameByCN;
:global UrlEncode;
:global WaitForFile;
:log info ("Downloading and importing certificate with " . \
"CommonName \"" . $CommonName . "\".");
:do {
:local LocalFileName ($CommonName . ".pem");
:local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
/ tool fetch check-certificate=yes-without-crl \
($ScriptUpdatesBaseUrl . "certs/" . \
$UrlFileName . $ScriptUpdatesUrlSuffix) \
dst-path=$LocalFileName;
$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={
:log warning "Failed imprting certificate!";
}
}
# check and download required certificate
:set CertificateAvailable do={
:local CommonName [ :tostr $1 ];
:global CertificateDownload;
:global ParseKeyValueStore;
:if ([ / system resource get free-hdd-space ] < 8388608 && \
[ / certificate settings get crl-download ] = true && \
[ / certificate settings get crl-store ] = "system") do={
:log warn "This system has low free flash space but is configured to download certificate CRLs to system!";
}
:if ([ / certificate print count-only where common-name=$CommonName ] = 0) do={
:log info ("Certificate with CommonName \"" . $CommonName . "\" not available.");
$CertificateDownload $CommonName;
}
:local CertVal;
:local Issuer $CommonName;
:do {
:if ([ / certificate print count-only where common-name=$Issuer ] = 0) do={
:log info ("Certificate chain for \"" . $CommonName . "\" is incomplete, missing \"" . $Issuer . "\".");
$CertificateDownload $CommonName;
}
:set CertVal [ / certificate get [ find where common-name=$Issuer ] ];
:set Issuer ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN");
} while=($Issuer != $CertVal->"common-name");
}
# send notification via e-mail
:set SendEMail do={
:local Subject [ :tostr $1 ];
:local Message [ :tostr $2 ];
:local Attach [ :tostr $3 ];
:global Identity;
:global EmailGeneralTo;
:global EmailGeneralCc;
:if ([ :len $EmailGeneralTo ] > 0) do={
:do {
:local Signature [ / system note get note ];
:if ([ :len $Signature ] > 0) do={
:set Signature ("\n-- \n" . $Signature);
}
/ tool e-mail send to=$EmailGeneralTo cc=$EmailGeneralCc \
subject=("[" . $Identity . "] " . $Subject) \
body=($Message . $Signature) file=$Attach;
} on-error={
:log warning "Failed sending notification mail!";
}
}
}
# send notification via telegram
:set SendTelegram do={
:local Subject [ :tostr $1 ];
:local Message [ :tostr $2 ];
:local Silent [ :tostr $3 ];
:global Identity;
:global TelegramTokenId;
:global TelegramChatId;
:global UrlEncode;
:global CertificateAvailable;
:if ([ :len $TelegramTokenId ] > 0 && [ :len $TelegramChatId ] > 0) do={
$CertificateAvailable "Go Daddy Secure Certificate Authority - G2";
:do {
/ tool fetch check-certificate=yes-without-crl output=none http-method=post \
("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
http-data=("chat_id=" . $TelegramChatId . "&disable_notification=" . $Silent . \
"&text=" . [ $UrlEncode ("[" . $Identity . "] " . $Subject . "\n\n" . $Message) ]);
} on-error={
:log warning "Failed sending telegram notification!";
}
}
}
# 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 Attach [ :tostr $3 ];
:local Silent [ :tostr $4 ];
:global SendEMail;
:global SendTelegram;
$SendEMail $Subject $Message $Attach;
$SendTelegram $Subject $Message $Silent;
}
# get MAC vendor
:set GetMacVendor do={
:local Mac [ :tostr $1 ];
:global CertificateAvailable;
:do {
:local Vendor;
$CertificateAvailable "Let's Encrypt Authority X3";
:set 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={
:return "unknown vendor";
}
}
# 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;
}
# 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 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") do={
:set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
}
:local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];
$CertificateAvailable "Let's Encrypt Authority X3";
: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={
# catch error and fall through
}
/ file remove [ find where name=$PkgDest ];
:set Retry ($Retry - 1);
}
:return false;
}
# lock script against multiple invocation
:set ScriptLock do={
:local Script [ :tostr $1 ];
:if ([ / system script job print count-only where script=$Script ] > 1) do={
:log debug ("Script " . $Script . " started more than once... Aborting.");
:error "Locked."
}
}
# check if script is run from terminal
:set ScriptFromTerminal do={
:local Script [ :tostr $1 ];
: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={
:log debug ("Script " . $Script . " started from terminal.");
:return true;
}
}
:return false;
}
# wait for file to be available
:set WaitForFile do={
:global CleanFilePath;
:local FileName [ $CleanFilePath [ :tostr $1 ] ];
:local I 0;
:while ([ file print count-only where name=$FileName ] = 0) do={
:if ($I > 20) do={
:return false;
}
:delay 100ms;
:set I ($I + 1);
}
:return true;
}
# parse key value store
:set ParseKeyValueStore do={
:global CharacterReplace;
:local Source $1;
:if ([ :typeof $Source ] != "array") do={
:set Source [ :tostr $1 ];
}
:local Result [ :toarray "" ];
:foreach KeyValue in=[ :toarray $Source ] do={
:set KeyValue [ :toarray [ $CharacterReplace $KeyValue "=" "," ] ];
:set ($Result->($KeyValue->0)) ($KeyValue->1);
}
:return $Result;
}
# generate random number
# Warning: This is a *very* weak algorithm and in *no way*
# useful for cryptography or similar!
:set GetRandom do={
:local Max ([ :tonum $1 ] + 1);
:local Sum 0;
:foreach Interface in=[ /interface find ] do={
:set Sum ($Sum + [ /interface get $Interface tx-byte ]);
}
:return ($Sum % $Max);
}
# delay a random amount of seconds
:set RandomDelay do={
:global GetRandom;
:delay ([ $GetRandom $1 ] . "s");
}
# get readable device info
:set DeviceInfo do={
:global ExpectedConfigVersion;
:global GlobalConfigVersion;
:global Identity;
:local Resource [ / system resource get ];
:local RouterBoard [ / system routerboard get ];
:local Update [ / system package update get ];
:local Info ( \
"Hostname: " . $Identity . "\n" . \
"Board name: " . $Resource->"board-name" . "\n" . \
"Architecture: " . $Resource->"architecture-name");
:if ($RouterBoard->"routerboard" = true) do={
:local Revision "";
:if ([ :len ($RouterBoard->"revision") ] > 0) do={
:set Revision (" " . $RouterBoard->"revision");
}
:set Info ($Info . "\n" . \
"Model: " . $RouterBoard->"model" . $Revision . "\n" . \
"Serial number: " . $RouterBoard->"serial-number");
}
:set Info ($Info . "\n" . \
"RouterOS:\n" . \
" Channel: " . $Update->"channel" . "\n" . \
" Installed: " . $Update->"installed-version");
:if ([ :typeof ($Update->"latest-version") ] != "nothing" && \
$Update->"installed-version" != $Update->"latest-version") do={
:set Info ($Info . "\n" . \
" Available: " . $Update->"latest-version");
}
:set Info ($Info . "\n" . \
"RouterOS-Scripts Configuration Version:\n" . \
" Current: " . $GlobalConfigVersion);
:if ($GlobalConfigVersion != $ExpectedConfigVersion) do={
:set Info ($Info . "\n" . \
" Expected: " . $ExpectedConfigVersion);
}
:return $Info;
}
# install new scripts, update existing scripts
:set ScriptInstallUpdate do={
:local Scripts [ :toarray $1 ];
:foreach Script in=$Scripts do={
:if ([ / system script print count-only where name=$Script ] = 0) do={
:log info ("Adding new script: " . $Script);
/ system script add name=$Script source="#!rsc";
}
}
/ system script run script-updates;
}
# check if mail server is up
:set MailServerIsUp do={
:local MailServer [ / tool e-mail get address ];
:local MailHost $MailServer;
:if ([ / tool netwatch print count-only where comment=$MailServer ] = 0) do={
:log warn ("Adding netwatch entry for mail server.");
:local MailHost $MailServer;
:if ([ :typeof [ :toip $MailHost ] ] != "ip" ) do={
:set MailHost [ :resolve $MailServer ];
}
/ tool netwatch add comment=$MailServer host=[ :resolve $MailServer ];
}
:if ([ / tool netwatch get [ find where comment=$MailServer ] status ] = "up") do={
:return true;
}
:return false;
}