Отказоустойчивый кластер из MikroTik’ов с Эдельвейсом

Допиливаем…

Зачем?

Как и в предидущем ремейке продолжаем глобализировать код с применением библиотеки Edelweiss. Логика и цели остаются теми же что и в оригинале, за исключением того, что все функции берутся из библиотеки. Пересобрал я этот скрипт уже достаточно давно, месяцев эдак 6 назад, но вот руки только сейчас дошли до публикации.

Русскоязычный онлайн-курс по MikroTik от нашего коллеги Дмитрия Скромнова. Здесь можно изучить MikroTik и RouterOS самостоятельно по курсу «Настройка оборудования MikroTik». Курс основан на официальной программе MTCNA, но содержит больше информации. Это 162 видеоурока и большая практическая задача, разбитая на 45 лабораторных работ. Время на изучение неограниченно – все материалы передаются бессрочно и их можно пересматривать сколько нужно. Первые 25 уроков можно посмотреть бесплатно, оставив заявку на странице курса.

Рецепт

Не будем повторяться, просто скрипт полностью с пояснениями и небольшими дополнениями. Напоминаю о том, что скрипт нужно добавить в Sheduler. Более полное описание работы и всех тонкостей можно найти здесь

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
beep frequency=1760 length=10ms;
:local scriptName "Failover";	# Задаем имя скрипта
:if ([len [/system script job find where script=$scriptName]]>1) do={	# Проверяем на наличее уже запущенного
# скрипта. Если скрипт уже запущен, выходим, если нет, работаем.
	log error "$scriptName: Single instance";
	beep frequency=440 length=1000ms;
} else={
	delay 60s;	# При старте вместе с системой лучше подождать, иначе возможны грабли, проверено.
	:local globalIPArray {"8.8.8.8";"77.88.8.8";"193.58.251.251";"114.114.115.115"};	# Определяем адреса
# которые скрипт пингует для проверки интернет соединения. Кол-во адресов может быть любое. В примере указано 4
# публичных DNS сервера (Google, Yandex, SkyDNS, 114dns).
	:local localIPArray {"10.0.0.4";"10.0.1.4";"10.0.2.4";"172.16.0.4";"192.168.0.4"};	# Определяем адреса
# которые скрипт пингует для проверки локального соединения. Кол-во адресов может быть любое.
	:local restoreDelays {600;7200;21600;43200;86400};	# Определяем тайм-ауты восстановления аплинка в
# секундах. Кол-во и значения могут быть любые.
	:local filePath "disk1/var/log/full_log.0.txt";	# Указываем файл высылаемый на почту при сбое.
	:local email "mail@example.com";	# Куда спамить письмами.
	:local displayOnRestore "uplink0";	# Что отображаем на дисплее в нормальном режиме.
	:local displayOnError "bond1.0";	# Что отображаем на дисплее при сбое.
	:local uplink "uplink0";	# Указываем интерфейс аплинка.
	:local pingCount 2;	# Кол-во пингов на один IP.
	:local conA 0;
	:local conB 0;
	:local conD 0;
	:local conC 0;
	:local once 0;
	:local pingResA 0;
	:local pingResB 0;
	:local mailSubject ([/system identity get name]."::".$scriptName);	# Тема отправляемого E-Mail.

# Вводим глобальные переменные для отображения текущего статуса в environment.
	:global FailoverState 0;	# Отображает текущий статус скрипта, всего их 5, от 0 до 4.
	:global UplinkIPCurrentScan "waiting...";	# Отображает IP в интернет который скрипт пингует в
# данный момент.
	:global LocalIPCurrentScan "waiting...";	# Отображает локальный IP  который скрипт пингует в
# данный момент.
	:global RestoreDelay 0;	# Номер следующего тайм-аута восстановления аплинка.
	:global RestoreDelayTime "0 s";	# Длительность следующего тайм-аута.
	:global RestoreDelayTimeRemaining "0 s";	# Отображает оставшеся время до восстановления аплинка
# без учёта длительности основного цикла.
	:global UplinkDrops 0;	# Количество потерянных пакетов на аплинке.
	:global UplinkDisabled "false";	# Статус аплинк интерфейса.
	:global UplinkFailoverTimes 0;	# Кол-во падений аплинка.
	:global UplinkFailoverLastTime "-";	# Дата и время последнего падения аплинка.
	:global UplinkRestoreTimes 0;	# Кол-во восстановлений аплинка.
	:global UplinkRestoreLastTime "-";	# Дата и время последнего восстановления аплинка.
	:global LocalDrops 0;	# Количество потерянных пакетов в локалке.
	:global LocalDisabled "false";	# Статус локального интерфейса.
	:global LocalFailoverTimes 0;	# Кол-во падений локалки.
	:global LocalFailoverLastTime "-";	# Дата и время последнего падения локалки.
	:global LocalRestoreTimes 0;	# Кол-во восстановлений локалки.
	:global LocalRestoreLastTime "-";	# Дата и время последнего восстановления локалки.

# Запускаем библиотеку и объявляем функции.	
	/system script run "Edelweiss";
	:global mlFrm;
	:global cuDte;
	:global beMel;
	:global foDte;
	:global ifSta;
	:global ifDis;
	:global ifRun;
 
 
# Функция изменяет параметр edge для всех портов мостов. Это лечит известные грабли с мостиками. 
# Примеры:
# [$brdgPt brdgEd="auto"]; - Изменяет значение параметра edge на заданный (в примере на auto);
	:local brdgPt do={
		:local brdgPtCount [/interface bridge port print count-only;];
		do {
			set $brdgPtCount ($brdgPtCount-1);
			:local F [/interface bridge port print brief];
			if ([/interface bridge port get $brdgPtCount edge] != $brdgDo) do={
				execute "/interface bridge port set edge=$brdgEd numbers=$brdgPtCount";
			}
		} while ($brdgPtCount>0);
	}
 
 
	lcd backlight state=on;	# Включаем дисплей.
	[$brdgPt brdgDo="yes"];	# "Подготавливаем" мосты.
	[/interface enable $uplink;]	# Включаем аплинк.
	lcd interface display $displayOnRestore;	# Отображаем заданный интерфейс.
	[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];	# Поднимаем VRRP интерфейсы
	[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"100"}) Length=({"100"})];	# Звучим о старте.
	/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Started.\n" file=$filePath;	# Шлём на почту файл (текущий лог) и сообщение о старте.

	if ($FailoverState=0) do={	# Проверяем текущий статус, если "0", то стартует основной цикл. Именно он держит скрипт
# в режиме постоянно активного процесса.

		[$beMel Frequency=({"1975"; "1975"}) Delay=({"1000"; "150"}) Length=({"100"})];	# Звучим о старте процесса.

		do {
			if ($conC<1) do={		# Если тайм-аут отсутствует (меньше 1).
				if ([/interface get $uplink running]=false) do={	# Проверяем состояние интерфейса.
					if ($once=0) do={	# Если выполняется впервые - действуем.
						set $once 1;	# Отмечаем как выполненное.
						log info "$scriptName: Uplink IF DOWN";
						set $UplinkFailoverLastTime "$[$cuDte]";
						set $UplinkFailoverTimes ($UplinkFailoverTimes+1);
						log info "$scriptName: state changed $FailoverState->1";
						set $FailoverState 1;	# Устанавливаем статус "1".
						[$beMel Frequency=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) Delay=({"150"}) Length=({"150"})];
						[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];	# Отключаем L2TP интерфейсы.
						[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];	# Отключаем PPTP интерфейсы.
						[$ifRun Name=$scriptName Type="vrrp" Disabled="yes"];	# Отключаем VRRP интерфейсы.
						[$brdgPt brdgDo="yes"];	# Дёргаем мосты.
						log info "$scriptName: state changed $FailoverState->4";
						set $FailoverState 4;	# Устанавливаем статус "4".
						lcd backlight state=off;	# Тушим подсветку сигнализируя тем самым о проблеме.
						# Шлём на почту файл (текущий лог) и сообщение о падении физики.
						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Uplink IF DOWN.\n" file=$filePath;
					}
				} else={
					if ($FailoverState=4) do={	# Проверяем статус "4".
						if ($once=1) do={	# Если выполнено предидущее действие - стартуем.
							set $once 0;	# Сбрасываем счётчик выполнения.
							log info "$scriptName: Uplink IF UP";
							set $UplinkRestoreLastTime "$[$cuDte]";
							set $UplinkRestoreTimes ($UplinkRestoreTimes+1);
							log info "$scriptName: state changed $FailoverState->3";
							set $FailoverState 3;	# Устанавливаем статус "3".
							[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"150"}) Length=({"150"})];
							[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];	# Включаем VRRP интерфейсы.
							if ([$ifSta Type="vrrp"]=true) do={	# Если все VRRP интерфейсы активны.
								[$brdgPt brdgEd="auto"]; 	# "Будим" мосты.
								[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];	# Включаем PPTP интерфейсы.
								[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];	# Включаем L2TP интерфейсы.
							}
							log info "$scriptName: state changed $FailoverState->0";
							set $FailoverState 0;	# Устанавливаем статус "0".
							lcd backlight state=on;	# Включаем дисплей для наглядности.
							# Шлём на почту файл (текущий лог) и сообщение о восттановлении физики.
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Uplink IF UP.\n" file=$filePath;
						}
					}
				}
 
 
				if ($FailoverState<4) do={	# Если статус меньше "4" то работаем.
					set $conA 0;
					set $pingResA 0;
					set $conB ([len $globalIPArray]-1);	# Определяем длинну массива с IP адресами.
					do {
						set $conA ($conA+$pingCount);	# Считаем количество пингов.
						set $UplinkIPCurrentScan [($globalIPArray->$conB)];	# Берём из массива IP адрес.
						set $pingResA ($pingResA+[ping $UplinkIPCurrentScan count=$pingCount]);	# Считаем кол-во ответов.
						set $conB ($conB-1);	# Уменьшакем счётчик цикла.
						delay "$(([len $globalIPArray]*$pingCount)*100) ms";	# Тайм-аут. Счиается из расчёта 100 мс на один пинг.
					} while ($conB>-1);
					set $UplinkDrops ($UplinkDrops+($conA-$pingResA));	# Считаем количество потерянных пакетов.
				}
 
 
				if ($pingResA=0) do={	# Если результат пинга равен нулю.
					if ($FailoverState=0) do={	# Если статус "0".
						log info "$scriptName: connection to the Internet lost";
						set $UplinkFailoverLastTime "$[$cuDte]";
						set $UplinkFailoverTimes ($UplinkFailoverTimes+1);
						log info "$scriptName: state changed $FailoverState->1";
						set $FailoverState 1;	# Устанавливаем статус "1".
						# Звучим о потере соединения.
						[$beMel Frequency=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) Delay=({"150"}) Length=({"150"})];
						[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];	# Отключаем L2TP интерфейсы.
						[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];	# Отключаем PPTP интерфейсы.
						[$ifRun Name=$scriptName Type="vrrp" Disabled="yes"];	# Отключаем VRRP интерфейсы.
						[$brdgPt brdgDo="yes"]; 	# "Будим" мосты.
						log info "$scriptName: state changed $FailoverState->2";
						set $FailoverState 2;	# Устанавливаем статус "2".
						lcd backlight state=off;	# Тушим подсветку сигнализируя тем самым о проблеме.
						# Шлём на почту файл (текущий лог) и сообщение об отсутствии подключения.
						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Connection to the Internet lost.";
					}				
				} else={	# Если результат пинга не равен нулю.
					if ($FailoverState=2) do={	# Если статус "2".
						log info "$scriptName: connection to the Internet established";
						set $UplinkRestoreLastTime "$[$cuDte]";
						set $UplinkRestoreTimes ($UplinkRestoreTimes+1);
						log info "$scriptName: state changed $FailoverState->3";
						set $FailoverState 3;	# Устанавливаем статус "3".
						# Звучим о восттановлении соединения.
						[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"150"}) Length=({"150"})];
						[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];	# Включаем VRRP интерфейсы.
						if ([$ifSta Type="vrrp"]=true) do={	# Если все VRRP интерфейсы активны.
							[$brdgPt brdgEd="auto"]; 	# "Будим" мосты.
							[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];	# Включаем PPTP интерфейсы.
							[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];	# Включаем L2TP интерфейсы.
						}
						log info "$scriptName: state changed $FailoverState->0";
						set $FailoverState 0;	# Устанавливаем статус "0".
						lcd backlight state=on;	# Включаем дисплей для наглядности.
						# Шлём на почту файл (текущий лог) и сообщение о восстановлении подключения.
#						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Connection to the Internet established.";
					}
				}
 
				# Принцип работы тайм-аутов следующий. Если "падений" более одного, то устанавливаем первый тайм-аут из заданных в массиве
				# и устанавливаем счётчик тайм-аутов равным единице. По истечению тайм-аута, пробуем использовать аплинк. Если попытка удалась,
				# то ждём истечения времени заданного в последней позиции массива для сброса счётчика тайм-аутов, если нет, то устанавливаем
				# следующий тайм-аут из массива и увеличиваем значение счётчика на единицу. Если все тайм-ауты израсходованы, то отключаем
				# аплинк-интерфейс и отсылаем письмо об ошибке.

				if ($FailoverState=2) do={	# Если статус "2".
					if ($UplinkFailoverTimes>1) do={	# Если "падений" более одного.
						set $conC 1;	# Устанавливаем наличие тайм-аута.
						set $rdLe [len $restoreDelays];	# Определяем длинну массива с тайм-аутами.
						set $RestoreDelay ($RestoreDelay+1);	# Устанавливаем номер следующего тайм-аута.
						# Если номер следующего тайм-аута больше длинны массива с тайм-аутами.
						if ($RestoreDelay>$rdLe) do={									
							set $FailoverState 4;	# Устанавливаем статус "4".
							# Устанавливаем время следующего восстановления "бесконечность".
							set $RestoreDelayTime "infinit"
							set $UplinkDisabled "true"; 	# Устанавливаем статус аплинка "отключен".
							[/interface disable $uplink;] 	# Отключаем аплинк интерфейс.
							# Отправляем письмо о том, что аплинк-интерфейс отключен.
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Error occurred! Uplink interface $uplink disabled.\n" file=$filePath;
						}
 
					}
				}
 
 
				if ($FailoverState=0) do={	# Если статус "0".
					if ($RestoreDelay>0) do={	# Если номер следующего тайм-аута больше нуля.
						# Если ( "текущая дата и время" минус "дата и время последнего восстановления аплинка" ) больше чем 
						# "последний тайм-аут в массиве тайма-утов".
						if ((([$foDte Date=[$cuDte]]->0)-([$foDte Date=$UplinkRestoreLastTime]->0))>[($restoreDelays->($rdLe-1))]) do={
							set $RestoreDelay 0;	# Устанавливаем номер следующего тайм-аута "0".
							set $RestoreDelayTime "0 s";	# Устанавливаем время следующего восстановления "0 секунд".
						}
					}
				}
 
				# Если "падений" более нуля, устанавливаем время следующего восстановления из массива тайм-аутов под номером
				# следующего тайм-аута.

				if ($UplinkFailoverTimes>0) do={set $RestoreDelayTime "$[($restoreDelays->$RestoreDelay)] s";}
 
 
			} else={	# Если тайм-аут присутствует (больше 0).
				# Если номер следующего тайм-аута больше нуля.
				if ($RestoreDelay>0) do={
					# Если ( "текущая дата и время" минус "дата и время последнего "падения" аплинка" ) больше чем
					# "текущее значение тайм-аута" в массиве тайма-утов, устанавливаем что тайм-аут отсутствует (значение "0").
					if ((([$foDte Date=[$cuDte]]->0)-([$foDte Date=$UplinkFailoverLastTime]->0))>[($restoreDelays->($RestoreDelay-1))]) do={set $conC 0;}
					# Если номер следующего тайм-аута равен нулю или меньше, устанавливаем что тайм-аут отсутствует (значение "0").
				} else={set $conC 0;}
			}
 
			# Вычисляем оставшееся время до окончания тайм-аута. 
			set $rdtrs ((([$foDte Date=$UplinkFailoverLastTime]->0)+[($restoreDelays->($RestoreDelay-1))])-([$foDte Date=[$cuDte]]->0));
			# Если оставшееся время до окончания тайм-аута больше нуля, то устанавливаем значение оставшегося времени равным оставшемуся времени,
			# если нет, то устанавливаем значение оставшегося времени равным нулю (выполняется один раз за цикл).
			if ($rdtrs>0) do={set $RestoreDelayTimeRemaining "$rdtrs s";} else={set $RestoreDelayTimeRemaining "0 s";}
 
 
			set $vicS [$ifSta Type="vrrp"];	# "Запоминаем" статус VRRP интерфейсов.

			if ([$ifSta Type="vrrp"]=true) do={	# Если все VRRP интерфейсы активны.
				[$brdgPt brdgEd="auto"]; 	# "Будим" мосты.
				[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];	# Включаем PPTP интерфейсы.
				[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];	# Включаем L2TP интерфейсы.
			} else={
				[$brdgPt brdgEd="yes"]; 	# "Будим" мосты.
				[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];	# Отключаем L2TP интерфейсы.
				[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];	# Отключаем PPTP интерфейсы.
			}
 
 
			set $conD ([len $localIPArray]-1);	# Определяем длинну массива с IP адресами.
			do {
				if ([$ifSta Type="vrrp"]=$vicS) do={	# Если изменений в конфигурации VRRP интерфейсов нет.

					set $LocalIPCurrentScan [($localIPArray->$conD)];	# Берём из массива IP адрес.
					set $pingResB [ping $LocalIPCurrentScan count=$pingCount];	# Считаем кол-во ответов.
					set $LocalDrops ($LocalDrops+($pingCount-$pingResB));	# Считаем количество потерянных пакетов.
					set $conD ($conD-1);
					delay "$(([len $localIPArray]*$pingCount)*100) ms";	# Тайм-аут. Счиается из расчёта 100 мс на один пинг.

					if ($pingResB=0) do={	# Если результат пинга равен нулю.
						if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=true) do={	# Если текущий DHCP сервер не активен.
							# Звучим об активации текущего встроенного DHCP сервера.
							[$beMel Frequency=({"1046.5"; "1568.0"; "1046.5"; "1568.5"}) Delay=({"150"}) Length=({"150"})];
							log info "$scriptName: connection to $LocalIPCurrentScan lost";
							[/ip dhcp-server enable $LocalIPCurrentScan;]	# Активируем текущий DHCP сервер.
							log info "$scriptName: enable dhcp-server $LocalIPCurrentScan";
							set $LocalFailoverLastTime "$[$cuDte]";
							set $LocalFailoverTimes ($LocalFailoverTimes+1);
							lcd interface display $displayOnError;
						}
					} else={	# Если результат пинга не равен нулю.
						if ($LocalFailoverTimes<20) do={	# Если потерь соединений меньше 20.
							if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=false) do={	# Если текущий DHCP сервер активен.
								# Звучим об отключении текущего встроенного DHCP сервера.
								[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"100"}) Length=({"100"})];
								log info "$scriptName: connection to $LocalIPCurrentScan established";
								[/ip dhcp-server disable $LocalIPCurrentScan;]	# Отключаем текущий DHCP сервер.
								log info "$scriptName: disable dhcp-server $LocalIPCurrentScan";
								set $LocalRestoreLastTime "$[$cuDte]";
								set $LocalRestoreTimes ($LocalRestoreTimes+1);
								lcd interface display $displayOnRestore;
							}
						} else={	# Если потерь соединений больше 20.
							set $LocalDisabled "true";	# Отключаем локальные сервисы.
							# Шлём на почту файл (текущий лог) и сообщение о том что локальные сервисы отключены.
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Multiple errors! Local services disabled!\n" file=$filePath;
							set $conD -1;	# Завершаем цикл.
						}
					}
 
				} else={	# Если произошли изменения в конфигурации VRRP интерфейсов.
					set $conD -1;	# Завершаем цикл.
				}
			} while ($conD>-1);
 
		} while (true);
	}
	# В случае сбоя в работе основного цикла, шлём письмо о сбое прикрепляя файл (текущий лог).
	/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Error occurred! Unexpected end of cycle.\n" file=$filePath;
 
};

Коротко о статусах

0 — Работа в штатном режиме.
1 — Утеряно соединение с интернет, переход в режим отключения интерфейсов.
2 — Интерфейсы отключены, режим ожидания возобновления соединенеия.
3 — Соединение с интернет восстановлено, переход в режим включения интерфейсов.
4 — В аплинк интерфейс не подключен кабель или проблемы с физическим соединением.

Напоминаю, что активным шлюзом является VRRP интерфейс, а это означает что он там, где есть интернет. Соответственно, если прописать статический маршрут для IP почтового сервера и DNS на IP VRRP интерфейса, то почта будет отправлена через активное соединение на соседнем маршрутизаторе.

default routes
Статические маршруты для отправки почты

Дополнительно хочу добавить, что для отправки почты в том виде, в котором это происходит в данном скрипте, необходимо внести соответствующие настройки в Tools -> Email.

Узнать подробно о работе VRRP и других протоколов, лучших практиках построения отказоустойчивых кластеров и систем высокой доступности, можно из русскоязычного онлайн-курса по MikroTik для самостоятельного изучения от Дмитрия Скромнова. Данный курс по MikroTik и RouterOS основан на официальной программе MTCNA, однако содержит намного больше полезной информации. 162 видеоурока и большая практическая задача, разбитая на 45 лабораторных работ. Время на изучение неограниченно – все материалы передаются бессрочно и их можно пересматривать сколько нужно. Первые 25 уроков можно посмотреть бесплатно, оставив заявку на странице курса.

Скрипт полностью без «слов»:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
beep frequency=1760 length=10ms;
:local scriptName "Failover";
:if ([len [/system script job find where script=$scriptName]]>1) do={
	log error "$scriptName: Single instance";
	beep frequency=440 length=1000ms;
} else={
	delay 60s;
	:local globalIPArray {"8.8.8.8";"77.88.8.8";"193.58.251.251";"114.114.115.115"};
	:local localIPArray {"10.0.0.4";"10.0.1.4";"10.0.2.4";"172.16.0.4";"192.168.0.4"};
	:local restoreDelays {600;7200;21600;43200;86400};
	:local filePath "disk1/var/log/full_log.0.txt";
	:local email "mail@example.com";
	:local displayOnRestore "uplink0";
	:local displayOnError "bond1.0";
	:local uplink "uplink0";
	:local pingCount 2;
	:local conA 0;
	:local conB 0;
	:local conD 0;
	:local conC 0;
	:local once 0;
	:local pingResA 0;
	:local pingResB 0;
	:local mailSubject ([/system identity get name]."::".$scriptName);
 
	:global FailoverState 0;
	:global UplinkIPCurrentScan "waiting...";
	:global LocalIPCurrentScan "waiting...";
	:global RestoreDelay 0;
	:global RestoreDelayTime "0 s";
	:global RestoreDelayTimeRemaining "0 s";
	:global UplinkDrops 0;
	:global UplinkDisabled "false";
	:global UplinkFailoverTimes 0;
	:global UplinkFailoverLastTime "-";
	:global UplinkRestoreTimes 0;
	:global UplinkRestoreLastTime "-";
	:global LocalDrops 0;
	:global LocalDisabled "false";
	:global LocalFailoverTimes 0;
	:global LocalFailoverLastTime "-";
	:global LocalRestoreTimes 0;
	:global LocalRestoreLastTime "-";
 
	/system script run "Edelweiss";
	:global mlFrm;
	:global cuDte;
	:global beMel;
	:global foDte;
	:global ifSta;
	:global ifDis;
	:global ifRun;
 
 
 
	:local brdgPt do={
		:local brdgPtCount [/interface bridge port print count-only;];
		do {
			set $brdgPtCount ($brdgPtCount-1);
			:local F [/interface bridge port print brief];
			if ([/interface bridge port get $brdgPtCount edge] != $brdgDo) do={
				execute "/interface bridge port set edge=$brdgEd numbers=$brdgPtCount";
			}
		} while ($brdgPtCount>0);
	}
 
 
	lcd backlight state=on;
	[$brdgPt brdgDo="yes"];
	[/interface enable $uplink;]
	lcd interface display $displayOnRestore;
	[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];
	[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"100"}) Length=({"100"})];
	/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Started.\n" file=$filePath;
 
	if ($FailoverState=0) do={
 
		[$beMel Frequency=({"1975"; "1975"}) Delay=({"1000"; "150"}) Length=({"100"})];
 
		do {
			if ($conC<1) do={	
				if ([/interface get $uplink running]=false) do={
					if ($once=0) do={
						set $once 1;
						log info "$scriptName: Uplink IF DOWN";
						set $UplinkFailoverLastTime "$[$cuDte]";
						set $UplinkFailoverTimes ($UplinkFailoverTimes+1);
						log info "$scriptName: state changed $FailoverState->1";
						set $FailoverState 1;
						[$beMel Frequency=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) Delay=({"150"}) Length=({"150"})];
						[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];
						[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];
						[$ifRun Name=$scriptName Type="vrrp" Disabled="yes"];
						[$brdgPt brdgDo="yes"];
						log info "$scriptName: state changed $FailoverState->4";
						set $FailoverState 4;
						lcd backlight state=off;
						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Uplink IF DOWN.\n" file=$filePath;
					}
				} else={
					if ($FailoverState=4) do={
						if ($once=1) do={
							set $once 0;
							log info "$scriptName: Uplink IF UP";
							set $UplinkRestoreLastTime "$[$cuDte]";
							set $UplinkRestoreTimes ($UplinkRestoreTimes+1);
							log info "$scriptName: state changed $FailoverState->3";
							set $FailoverState 3;
							[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"150"}) Length=({"150"})];
							[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];
							if ([$ifSta Type="vrrp"]=true) do={
								[$brdgPt brdgEd="auto"];
								[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];
								[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];
							}
							log info "$scriptName: state changed $FailoverState->0";
							set $FailoverState 0;
							lcd backlight state=on;
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Uplink IF UP.\n" file=$filePath;
						}
					}
				}
 
 
				if ($FailoverState<4) do={
					set $conA 0;
					set $pingResA 0;
					set $conB ([len $globalIPArray]-1);
					do {
						set $conA ($conA+$pingCount);
						set $UplinkIPCurrentScan [($globalIPArray->$conB)];
						set $pingResA ($pingResA+[ping $UplinkIPCurrentScan count=$pingCount]);
						set $conB ($conB-1);
						delay "$(([len $globalIPArray]*$pingCount)*100) ms";
					} while ($conB>-1);
					set $UplinkDrops ($UplinkDrops+($conA-$pingResA));
				}
 
 
				if ($pingResA=0) do={
					if ($FailoverState=0) do={
						log info "$scriptName: connection to the Internet lost";
						set $UplinkFailoverLastTime "$[$cuDte]";
						set $UplinkFailoverTimes ($UplinkFailoverTimes+1);
						log info "$scriptName: state changed $FailoverState->1";
						set $FailoverState 1;
						[$beMel Frequency=({"1046.5"; "1318.5"; "1568.0"; "1760.0"; "1975.0"}) Delay=({"150"}) Length=({"150"})];
						[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];
						[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];
						[$ifRun Name=$scriptName Type="vrrp" Disabled="yes"];
						[$brdgPt brdgDo="yes"];
						log info "$scriptName: state changed $FailoverState->2";
						set $FailoverState 2;
						lcd backlight state=off;
						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Connection to the Internet lost.";
					}				
				} else={
					if ($FailoverState=2) do={
						log info "$scriptName: connection to the Internet established";
						set $UplinkRestoreLastTime "$[$cuDte]";
						set $UplinkRestoreTimes ($UplinkRestoreTimes+1);
						log info "$scriptName: state changed $FailoverState->3";
						set $FailoverState 3;
						[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"150"}) Length=({"150"})];
						[$ifRun Name=$scriptName Type="vrrp" Disabled="no"];
						if ([$ifSta Type="vrrp"]=true) do={
							[$brdgPt brdgEd="auto"];
							[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];
							[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];
						}
						log info "$scriptName: state changed $FailoverState->0";
						set $FailoverState 0;
						lcd backlight state=on;
#						/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Connection to the Internet established.";
					}
				}
 
 
 
				if ($FailoverState=2) do={
					if ($UplinkFailoverTimes>1) do={
						set $conC 1;
						set $rdLe [len $restoreDelays];
						set $RestoreDelay ($RestoreDelay+1);
 
						if ($RestoreDelay>$rdLe) do={									
							set $FailoverState 4;
							set $RestoreDelayTime "infinit"
							set $UplinkDisabled "true";
							[/interface disable $uplink;]
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Error occurred! Uplink interface $uplink disabled.\n" file=$filePath;
						}
 
					}
				}
 
 
				if ($FailoverState=0) do={
					if ($RestoreDelay>0) do={
						if ((([$foDte Date=[$cuDte]]->0)-([$foDte Date=$UplinkRestoreLastTime]->0))>[($restoreDelays->($rdLe-1))]) do={
							set $RestoreDelay 0;
							set $RestoreDelayTime "0 s";
						}
					}
				}
 
 
				if ($UplinkFailoverTimes>0) do={set $RestoreDelayTime "$[($restoreDelays->$RestoreDelay)] s";}
 
 
			} else={
				if ($RestoreDelay>0) do={
					if ((([$foDte Date=[$cuDte]]->0)-([$foDte Date=$UplinkFailoverLastTime]->0))>[($restoreDelays->($RestoreDelay-1))]) do={set $conC 0;}
				} else={set $conC 0;}
			}
 
			set $rdtrs ((([$foDte Date=$UplinkFailoverLastTime]->0)+[($restoreDelays->($RestoreDelay-1))])-([$foDte Date=[$cuDte]]->0));
			if ($rdtrs>0) do={set $RestoreDelayTimeRemaining "$rdtrs s";} else={set $RestoreDelayTimeRemaining "0 s";}
 
 
			set $vicS [$ifSta Type="vrrp"];
 
			if ([$ifSta Type="vrrp"]=true) do={
				[$brdgPt brdgEd="auto"];
				[$ifDis Name=$scriptName Type="pptp-out" Disabled="no"];
				[$ifDis Name=$scriptName Type="l2tp-out" Disabled="no"];
			} else={
				[$brdgPt brdgEd="yes"];
				[$ifDis Name=$scriptName Type="l2tp-out" Disabled="yes"];
				[$ifDis Name=$scriptName Type="pptp-out" Disabled="yes"];
			}
 
			set $conD ([len $localIPArray]-1);
			do {
				if ([$ifSta Type="vrrp"]=$vicS) do={
 
					set $LocalIPCurrentScan [($localIPArray->$conD)];
					set $pingResB [ping $LocalIPCurrentScan count=$pingCount];
					set $LocalDrops ($LocalDrops+($pingCount-$pingResB));
					set $conD ($conD-1);
					delay "$(([len $localIPArray]*$pingCount)*100) ms";
 
					if ($pingResB=0) do={
						if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=true) do={
							[$beMel Frequency=({"1046.5"; "1568.0"; "1046.5"; "1568.5"}) Delay=({"150"}) Length=({"150"})];
							log info "$scriptName: connection to $LocalIPCurrentScan lost";
							[/ip dhcp-server enable $LocalIPCurrentScan;]
							log info "$scriptName: enable dhcp-server $LocalIPCurrentScan";
							set $LocalFailoverLastTime "$[$cuDte]";
							set $LocalFailoverTimes ($LocalFailoverTimes+1);
							lcd interface display $displayOnError;
						}
					} else={
						if ($LocalFailoverTimes<20) do={
							if ([/ip dhcp-server get $LocalIPCurrentScan disabled]=false) do={
								[$beMel Frequency=({"1975.0"; "1760.0"; "1568.0"; "1318.5"; "1046.5"}) Delay=({"100"}) Length=({"100"})];
								log info "$scriptName: connection to $LocalIPCurrentScan established";
								[/ip dhcp-server disable $LocalIPCurrentScan;]
								log info "$scriptName: disable dhcp-server $LocalIPCurrentScan";
								set $LocalRestoreLastTime "$[$cuDte]";
								set $LocalRestoreTimes ($LocalRestoreTimes+1);
								lcd interface display $displayOnRestore;
							}
						} else={
							set $LocalDisabled "true";
							/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Multiple errors! Local services disabled!\n" file=$filePath;
							set $conD -1;
						}
					}
 
				} else={
					set $conD -1;
				}
			} while ($conD>-1);
 
		} while (true);
	}
 
	/tool e-mail send from=$mlFrm to=$email subject=$mailSubject body="$mailSubject:: $[$cuDte]:: Error occurred! Unexpected end of cycle.\n" file=$filePath;
 
};
Отказоустойчивый кластер из MikroTik’ов с Эдельвейсом

4 мыслей о “Отказоустойчивый кластер из MikroTik’ов с Эдельвейсом

  • 13.04.2018 в 00:30
    Permalink

    У меня только один вопрос. Вы от провайдера по 2 кабеля загоняете или кабель в свитч, там вланом до одного микротика и вланом до второго. Иначе я не вижу логики отказоустойчивости при одновременной работе и распределении траффика (load balancing)

    Ответить
    • 13.04.2018 в 10:19
      Permalink

      Здравствуйте. Нет, два провайдера, по одному в каждый маршрутизатор. И распределения нагрузки здесь нет, только отказоустойчивость.

      Ответить
  • 24.06.2018 в 20:44
    Permalink

    Добрый день!
    А зачем для резервирования собирать кластер, вместо банальной маркировки пакетов с несколькими шлюзами и т.д. внутри одной коробки?

    Ответить
    • 24.06.2018 в 20:59
      Permalink

      Добрый. Для того, чтобы когда одна «коробка» перестаёт работать, вторая брала на себя её задачи. Для «дома» оно конечно не нужно, можно и дистансами дефроутов отработать в «одной коробке», но здесь стояла совершенно иная задача.

      Ответить

Добавить комментарий для admin Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.