mirror of
https://github.com/eworm-de/routeros-scripts.git
synced 2026-01-14 04:59:32 +00:00
Compare commits
11 commits
main
...
change-76-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c03da561cd | ||
|
|
8d49babb01 | ||
|
|
b2f45c0350 | ||
|
|
52bd08b750 | ||
|
|
221b0409d5 | ||
|
|
0a9d0473f3 | ||
|
|
9024e20c0b | ||
|
|
5363df3568 | ||
|
|
25338ca384 | ||
|
|
ee9818e34d | ||
|
|
7d5c967995 |
9 changed files with 62 additions and 33 deletions
|
|
@ -108,7 +108,7 @@ $LogPrintExit2 debug $0 ("Checking for updates...") false;
|
||||||
|
|
||||||
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
||||||
:put ("Do you want to install RouterOS version " . $Update->"latest-version" . "? [y/N]");
|
:put ("Do you want to install RouterOS version " . $Update->"latest-version" . "? [y/N]");
|
||||||
:if (([ :terminal inkey timeout=60 ] % 32) = 25) do={
|
:if (([ / terminal inkey timeout=60 ] % 32) = 25) do={
|
||||||
$DoUpdate;
|
$DoUpdate;
|
||||||
} else={
|
} else={
|
||||||
:put "Canceled...";
|
:put "Canceled...";
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,8 @@ The hosts to be checked have to be added to netwatch with specific comment:
|
||||||
|
|
||||||
/ tool netwatch add comment="notify, hostname=example.com" host=[ :resolve "example.com" ];
|
/ tool netwatch add comment="notify, hostname=example.com" host=[ :resolve "example.com" ];
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
It is possible to run an up hook command (`up-hook`) or down hook command
|
It is possible to run an up hook command (`up-hook`) or down hook command
|
||||||
(`down-hook`) when a notification is triggered. This has to be added in
|
(`down-hook`) when a notification is triggered. This has to be added in
|
||||||
comment, note that some characters need extra escaping:
|
comment, note that some characters need extra escaping:
|
||||||
|
|
@ -48,10 +50,14 @@ Also there is a `pre-down-hook` that fires at two thirds of failed checks
|
||||||
required for the notification. The idea is to fix the issue before a
|
required for the notification. The idea is to fix the issue before a
|
||||||
notification is sent.
|
notification is sent.
|
||||||
|
|
||||||
|
### Count threshould
|
||||||
|
|
||||||
The count threshould (default is 5 checks) is configurable as well:
|
The count threshould (default is 5 checks) is configurable as well:
|
||||||
|
|
||||||
/ tool netwatch add comment="notify, hostname=example.com, count=10" host=104.18.144.11;
|
/ tool netwatch add comment="notify, hostname=example.com, count=10" host=104.18.144.11;
|
||||||
|
|
||||||
|
### Parents & dependencies
|
||||||
|
|
||||||
If the host is behind another checked host add a dependency, this will
|
If the host is behind another checked host add a dependency, this will
|
||||||
suppress notification if the parent host is down:
|
suppress notification if the parent host is down:
|
||||||
|
|
||||||
|
|
@ -61,6 +67,8 @@ suppress notification if the parent host is down:
|
||||||
Note that every configured parent in a chain increases the check count
|
Note that every configured parent in a chain increases the check count
|
||||||
threshould by one.
|
threshould by one.
|
||||||
|
|
||||||
|
### Update from DNS
|
||||||
|
|
||||||
The host address can be updated dynamically. Give extra parameter `resolve`
|
The host address can be updated dynamically. Give extra parameter `resolve`
|
||||||
with a resolvable name:
|
with a resolvable name:
|
||||||
|
|
||||||
|
|
@ -70,6 +78,16 @@ But be warned: Dynamic updates will probably cause issues if the name has
|
||||||
more than one record in dns - a high rate of configuration changes (and flash
|
more than one record in dns - a high rate of configuration changes (and flash
|
||||||
writes) at least.
|
writes) at least.
|
||||||
|
|
||||||
|
### No notification on host down
|
||||||
|
|
||||||
|
Also suppressing the notification on host down is possible with parameter
|
||||||
|
`no-down-notification`. This may be desired for devices that are usually
|
||||||
|
powered off, but accessibility is of interest.
|
||||||
|
|
||||||
|
/ tool netwatch add comment="notify, hostname=printer, no-down-notification" host=10.0.0.30;
|
||||||
|
|
||||||
|
Go and get your coffee ☕️ before sending the print job.
|
||||||
|
|
||||||
Also notification settings are required for e-mail, matrix and/or telegram.
|
Also notification settings are required for e-mail, matrix and/or telegram.
|
||||||
|
|
||||||
Tips & Tricks
|
Tips & Tricks
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ See also
|
||||||
--------
|
--------
|
||||||
|
|
||||||
* [Notify on RouterOS update](check-routeros-update.md)
|
* [Notify on RouterOS update](check-routeros-update.md)
|
||||||
|
* [Upload backup to Mikrotik cloud](backup-cloud.md)
|
||||||
* [Send backup via e-mail](backup-email.md)
|
* [Send backup via e-mail](backup-email.md)
|
||||||
* [Upload backup to server](backup-upload.md)
|
* [Upload backup to server](backup-upload.md)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
# Make sure all configuration properties are up to date and this
|
# Make sure all configuration properties are up to date and this
|
||||||
# value is in sync with value in script 'global-functions'!
|
# value is in sync with value in script 'global-functions'!
|
||||||
:global GlobalConfigVersion 74;
|
:global GlobalConfigVersion 76;
|
||||||
|
|
||||||
# This is used for DNS and backup file.
|
# This is used for DNS and backup file.
|
||||||
:global Domain "example.com";
|
:global Domain "example.com";
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@
|
||||||
# Make sure all configuration properties are up to date and this
|
# Make sure all configuration properties are up to date and this
|
||||||
# value is in sync with value in script 'global-functions'!
|
# value is in sync with value in script 'global-functions'!
|
||||||
# Comment or remove to disable news and change notifications.
|
# Comment or remove to disable news and change notifications.
|
||||||
:global GlobalConfigVersion 74;
|
:global GlobalConfigVersion 76;
|
||||||
|
|
||||||
|
# Use branch routeros-v6 with RouterOS v6:
|
||||||
|
:global ScriptUpdatesUrlSuffix "\?h=routeros-v6";
|
||||||
|
|
||||||
# Copy configuration from global-config here and modify it.
|
# Copy configuration from global-config here and modify it.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@
|
||||||
72="Introduced new script 'netwatch-dns' to manage DNS and DoH servers from netwatch.";
|
72="Introduced new script 'netwatch-dns' to manage DNS and DoH servers from netwatch.";
|
||||||
73="Renamed backup scripts ('cloud-backup' -> 'backup-cloud', 'email-backup' -> 'backup-email', 'upload-backup' -> 'backup-upload').";
|
73="Renamed backup scripts ('cloud-backup' -> 'backup-cloud', 'email-backup' -> 'backup-email', 'upload-backup' -> 'backup-upload').";
|
||||||
74="Extended 'hotspot-to-wpa', it can now read additional configuration from templates and hotspot users.";
|
74="Extended 'hotspot-to-wpa', it can now read additional configuration from templates and hotspot users.";
|
||||||
|
75="You are using the branch 'routeros-v6', well done.";
|
||||||
|
76="Added an option to suppress notifications on host down with 'netwatch-notify'.";
|
||||||
};
|
};
|
||||||
|
|
||||||
# Migration steps to be applied on script updates
|
# Migration steps to be applied on script updates
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
# https://git.eworm.de/cgit/routeros-scripts/about/
|
# https://git.eworm.de/cgit/routeros-scripts/about/
|
||||||
|
|
||||||
# expected configuration version
|
# expected configuration version
|
||||||
:global ExpectedConfigVersion 74;
|
:global ExpectedConfigVersion 76;
|
||||||
|
|
||||||
# global variables not to be changed by user
|
# global variables not to be changed by user
|
||||||
:global GlobalFunctionsReady false;
|
:global GlobalFunctionsReady false;
|
||||||
|
|
@ -252,6 +252,7 @@
|
||||||
:global CleanFilePath;
|
:global CleanFilePath;
|
||||||
:global LogPrintExit2;
|
:global LogPrintExit2;
|
||||||
:global MkDir;
|
:global MkDir;
|
||||||
|
:global VersionToNum;
|
||||||
:global WaitForFile;
|
:global WaitForFile;
|
||||||
|
|
||||||
:if ([ :len $PkgName ] = 0) do={ :return false; }
|
:if ([ :len $PkgName ] = 0) do={ :return false; }
|
||||||
|
|
@ -259,6 +260,9 @@
|
||||||
:if ([ :len $PkgArch ] = 0) do={ :set PkgArch [ / system resource get architecture-name ]; }
|
:if ([ :len $PkgArch ] = 0) do={ :set PkgArch [ / system resource get architecture-name ]; }
|
||||||
|
|
||||||
:local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk");
|
:local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk");
|
||||||
|
:if ([ $VersionToNum $PkgVer ] < [ $VersionToNum "7.0" ] && $PkgName = "routeros") do={
|
||||||
|
:set PkgFile ($PkgName . "-" . $PkgArch . "-" . $PkgVer . ".npk");
|
||||||
|
}
|
||||||
:if ($PkgArch = "x86_64" || $PkgName ~ "^routeros-") do={
|
:if ($PkgArch = "x86_64" || $PkgName ~ "^routeros-") do={
|
||||||
:set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
|
:set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
|
||||||
}
|
}
|
||||||
|
|
@ -278,13 +282,13 @@
|
||||||
$LogPrintExit2 error $0 ("Downloading required certificate failed.") true;
|
$LogPrintExit2 error $0 ("Downloading required certificate failed.") true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:local Url ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile);
|
||||||
$LogPrintExit2 info $0 ("Downloading package file '" . $PkgName . "'...") false;
|
$LogPrintExit2 info $0 ("Downloading package file '" . $PkgName . "'...") false;
|
||||||
|
$LogPrintExit2 debug $0 ("... from url: " . $Url) false;
|
||||||
:local Retry 3;
|
:local Retry 3;
|
||||||
:while ($Retry > 0) do={
|
:while ($Retry > 0) do={
|
||||||
:do {
|
:do {
|
||||||
/ tool fetch check-certificate=yes-without-crl \
|
/ tool fetch check-certificate=yes-without-crl $Url dst-path=$PkgDest;
|
||||||
("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile) \
|
|
||||||
dst-path=$PkgDest;
|
|
||||||
$WaitForFile $PkgDest;
|
$WaitForFile $PkgDest;
|
||||||
|
|
||||||
:if ([ / file get [ find where name=$PkgDest ] type ] = "package") do={
|
:if ([ / file get [ find where name=$PkgDest ] type ] = "package") do={
|
||||||
|
|
@ -719,28 +723,28 @@
|
||||||
:foreach Scheduler in=[ / system scheduler find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={
|
:foreach Scheduler in=[ / system scheduler find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={
|
||||||
:local SchedulerVal [ / system scheduler get $Scheduler ];
|
:local SchedulerVal [ / system scheduler get $Scheduler ];
|
||||||
:if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={
|
:if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={
|
||||||
$LogPrintExit2 warning $0 ("Policies differ for script " . $ScriptVal->"name" . \
|
$LogPrintExit2 warning $0 ("Policies differ for script '" . $ScriptVal->"name" . \
|
||||||
" and its scheduler " . $SchedulerVal->"name" . "!") false;
|
"' and its scheduler '" . $SchedulerVal->"name" . "'!") false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
|
:if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
|
||||||
:local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ];
|
:local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ];
|
||||||
:if (!($Comment->"ignore" = true)) do={
|
:if (!($Comment->"ignore" = true)) do={
|
||||||
$LogPrintExit2 debug $0 ("Fetching script from url: " . $ScriptVal->"name") false;
|
|
||||||
:do {
|
:do {
|
||||||
:local BaseUrl $ScriptUpdatesBaseUrl;
|
:local BaseUrl $ScriptUpdatesBaseUrl;
|
||||||
:local UrlSuffix $ScriptUpdatesUrlSuffix;
|
:local UrlSuffix $ScriptUpdatesUrlSuffix;
|
||||||
:if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); }
|
:if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); }
|
||||||
:if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); }
|
:if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); }
|
||||||
|
:local Url ($BaseUrl . $ScriptVal->"name" . $UrlSuffix);
|
||||||
|
|
||||||
:local Result [ / tool fetch check-certificate=yes-without-crl \
|
$LogPrintExit2 debug $0 ("Fetching script '" . $ScriptVal->"name" . "' from url: " . $Url) false;
|
||||||
($BaseUrl . $ScriptVal->"name" . $UrlSuffix) output=user as-value ];
|
:local Result [ / tool fetch check-certificate=yes-without-crl $Url output=user as-value ];
|
||||||
:if ($Result->"status" = "finished") do={
|
:if ($Result->"status" = "finished") do={
|
||||||
:set SourceNew ($Result->"data");
|
:set SourceNew ($Result->"data");
|
||||||
}
|
}
|
||||||
} on-error={
|
} on-error={
|
||||||
$LogPrintExit2 warning $0 ("Failed fetching " . $ScriptVal->"name") false;
|
$LogPrintExit2 warning $0 ("Failed fetching script '" . $ScriptVal->"name" . "'!") false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -761,18 +765,18 @@
|
||||||
:set ReloadGlobalFunctions true;
|
:set ReloadGlobalFunctions true;
|
||||||
}
|
}
|
||||||
} else={
|
} else={
|
||||||
$LogPrintExit2 warning $0 ("Syntax validation for script " . $ScriptVal->"name" . \
|
$LogPrintExit2 warning $0 ("Syntax validation for script '" . $ScriptVal->"name" . \
|
||||||
" failed! Ignoring!") false;
|
"' failed! Ignoring!") false;
|
||||||
}
|
}
|
||||||
} else={
|
} else={
|
||||||
$LogPrintExit2 warning $0 ("Looks like new script " . $ScriptVal->"name" . \
|
$LogPrintExit2 warning $0 ("Looks like new script '" . $ScriptVal->"name" . \
|
||||||
" is not valid (missing shebang). Ignoring!") false;
|
"' is not valid (missing shebang). Ignoring!") false;
|
||||||
}
|
}
|
||||||
} else={
|
} else={
|
||||||
$LogPrintExit2 debug $0 ("Script " . $ScriptVal->"name" . " did not change.") false;
|
$LogPrintExit2 debug $0 ("Script '" . $ScriptVal->"name" . "' did not change.") false;
|
||||||
}
|
}
|
||||||
} else={
|
} else={
|
||||||
$LogPrintExit2 debug $0 ("No update for script " . $ScriptVal->"name" . ".") false;
|
$LogPrintExit2 debug $0 ("No update for script '" . $ScriptVal->"name" . "'.") false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -800,11 +804,10 @@
|
||||||
:global GlobalConfigMigration;
|
:global GlobalConfigMigration;
|
||||||
:local ChangeLogCode;
|
:local ChangeLogCode;
|
||||||
|
|
||||||
$LogPrintExit2 debug $0 ("Fetching news, changes and migration.") false;
|
|
||||||
:do {
|
:do {
|
||||||
:local Result [ / tool fetch check-certificate=yes-without-crl \
|
:local Url ($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix);
|
||||||
($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix) \
|
$LogPrintExit2 debug $0 ("Fetching news, changes and migration: " . $Url) false;
|
||||||
output=user as-value ];
|
:local Result [ / tool fetch check-certificate=yes-without-crl $Url output=user as-value ];
|
||||||
:if ($Result->"status" = "finished") do={
|
:if ($Result->"status" = "finished") do={
|
||||||
:set ChangeLogCode ($Result->"data");
|
:set ChangeLogCode ($Result->"data");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -122,10 +122,10 @@ $ScriptLock $0;
|
||||||
:set Parent ($NetwatchNotify->$Parent->"parent");
|
:set Parent ($NetwatchNotify->$Parent->"parent");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$LogPrintExit2 info $0 ("Host " . $HostName . " (" . $HostVal->"host" . ") is down for " . \
|
$LogPrintExit2 [ $IfThenElse ($HostInfo->"no-down-notification" != true) info debug ] $0 \
|
||||||
$Metric->"count" . " checks, " . [ $IfThenElse ($ParentNotified = false) [ $IfThenElse \
|
("Host " . $HostName . " (" . $HostVal->"host" . ") is down for " . $Metric->"count" . " checks, " . \
|
||||||
($Metric->"notified" = true) ("already notified.") ($Count - $Metric->"count" . " to go.") ] \
|
[ $IfThenElse ($ParentNotified = false) [ $IfThenElse ($Metric->"notified" = true) ("already notified.") \
|
||||||
("parent host " . $Parent . " is down.") ]) false;
|
($Count - $Metric->"count" . " to go.") ] ("parent host " . $Parent . " is down.") ]) false;
|
||||||
:if ((($Count * 2) - ($Metric->"count" * 3)) / 2 = 0 && [ :typeof ($HostInfo->"pre-down-hook") ] = "str") do={
|
:if ((($Count * 2) - ($Metric->"count" * 3)) / 2 = 0 && [ :typeof ($HostInfo->"pre-down-hook") ] = "str") do={
|
||||||
$NetwatchNotifyHook $HostName "pre-down" ($HostInfo->"pre-down-hook");
|
$NetwatchNotifyHook $HostName "pre-down" ($HostInfo->"pre-down-hook");
|
||||||
}
|
}
|
||||||
|
|
@ -134,9 +134,11 @@ $ScriptLock $0;
|
||||||
:if ([ :typeof ($HostInfo->"down-hook") ] = "str") do={
|
:if ([ :typeof ($HostInfo->"down-hook") ] = "str") do={
|
||||||
:set Message ($Message . "\n\n" . [ $NetwatchNotifyHook $HostName "down" ($HostInfo->"down-hook") ]);
|
:set Message ($Message . "\n\n" . [ $NetwatchNotifyHook $HostName "down" ($HostInfo->"down-hook") ]);
|
||||||
}
|
}
|
||||||
$SendNotification2 ({ origin=$0; \
|
:if ($HostInfo->"no-down-notification" != true) do={
|
||||||
subject=([ $SymbolForNotification "cross-mark" ] . "Netwatch Notify: " . $HostName . " down"); \
|
$SendNotification2 ({ origin=$0; \
|
||||||
message=$Message });
|
subject=([ $SymbolForNotification "cross-mark" ] . "Netwatch Notify: " . $HostName . " down"); \
|
||||||
|
message=$Message });
|
||||||
|
}
|
||||||
:set ($Metric->"notified") true;
|
:set ($Metric->"notified") true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ $ScriptLock $0;
|
||||||
:if ($NumInstalled > $NumLatest) do={
|
:if ($NumInstalled > $NumLatest) do={
|
||||||
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
||||||
:put "Latest version is older than installed one. Want to downgrade? [y/N]";
|
:put "Latest version is older than installed one. Want to downgrade? [y/N]";
|
||||||
:if (([ :terminal inkey timeout=60 ] % 32) = 25) do={
|
:if (([ / terminal inkey timeout=60 ] % 32) = 25) do={
|
||||||
:set DoDowngrade true;
|
:set DoDowngrade true;
|
||||||
} else={
|
} else={
|
||||||
:put "Canceled...";
|
:put "Canceled...";
|
||||||
|
|
@ -61,7 +61,7 @@ $ScriptLock $0;
|
||||||
$LogPrintExit2 warning $0 ("Running backup script " . $ScriptName . " before update failed!") false;
|
$LogPrintExit2 warning $0 ("Running backup script " . $ScriptName . " before update failed!") false;
|
||||||
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
||||||
:put "Do you want to continue anyway? [y/N]";
|
:put "Do you want to continue anyway? [y/N]";
|
||||||
:if (([ :terminal inkey timeout=60 ] % 32) = 25) do={
|
:if (([ / terminal inkey timeout=60 ] % 32) = 25) do={
|
||||||
$LogPrintExit2 info $0 ("User requested to continue anyway.") false;
|
$LogPrintExit2 info $0 ("User requested to continue anyway.") false;
|
||||||
} else={
|
} else={
|
||||||
$LogPrintExit2 info $0 ("Canceled update...") true;
|
$LogPrintExit2 info $0 ("Canceled update...") true;
|
||||||
|
|
@ -80,7 +80,7 @@ $ScriptLock $0;
|
||||||
|
|
||||||
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
:if ([ $ScriptFromTerminal $0 ] = true) do={
|
||||||
:put "Do you want to (s)chedule reboot or (r)eboot now? [s/R]";
|
:put "Do you want to (s)chedule reboot or (r)eboot now? [s/R]";
|
||||||
:if (([ :terminal inkey timeout=60 ] % 32) = 19) do={
|
:if (([ / terminal inkey timeout=60 ] % 32) = 19) do={
|
||||||
/ system scheduler add name="reboot-for-update" start-time=03:00:00 interval=1d \
|
/ system scheduler add name="reboot-for-update" start-time=03:00:00 interval=1d \
|
||||||
on-event=(":global RandomDelay; \$RandomDelay 3600; " . \
|
on-event=(":global RandomDelay; \$RandomDelay 3600; " . \
|
||||||
"/ system scheduler remove reboot-for-update; / system reboot;");
|
"/ system scheduler remove reboot-for-update; / system reboot;");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue