понедельник, 7 ноября 2022 г.

Mikrotik script как скрипт управления D-Link`ом по SNMP писали

Есть у меня сервер, с воткнутым в него свистком, который принимал смс. По крону смски проверялись и выполнялись определенные операции при поступлении управляющих кодов. Последнее время модем начал глючить, да и баланс уже год не показывает и сервер состарился, а тут еще и wAP LTE освободился. Я точно знаю что можно с него (аппарат на базе R11e-LTE) читать  и отправлять SMS, и даже USSD запросы.

Веруя в могучесть Mikrotik`а начал переписывать скрипт с bash под Mikrotik, но не с того конца. К тому моменту как я дошел до обработки управляющих кодов, выяснилось что Mikrotik не может управлять другими устройствами по SNMP О_О...

Почесал репу и решил поискать другие пути, помню у Mikrotik есть ssh и я уже неоднократно с него подключался на сервера, таки да, есть и работает. 

- Так, а SSH ключ поддерживается?, мне же надо будет запускать в фоне, где пароль не введешь
- Да, есть
- А что это за SSH private key?
- Не поверишь, это то, что тебе нужно!

Помучавшись (редко пользую ключи, хоть давно и хочу, и каждый раз это новый квест) я все таки загрузил ключ сгенерированный в Puttygen, естественно без ошибок не получилось:

unable to load key file (incorrect passphrase?) !

Собственно решение оказалось банальным:

ssh-keygen -i -f  MyKey.pub > MyKey2.pub

И вот теперь импорт прошел успешно. Главное запомнить для себя, в authorized_keys нужно указать пользователя, под которым приватный будет пытаться зайти и этот пользователь должен существовать на конечном сервере

Пишем скрипт дальше, мысль заключается в том, чтобы отправлять на любой Linux сервер команду snmpset с необходимыми параметрами.

В каком-то месте возникла проблема с массивами и еще

В виду долгого исполнения некоторых команд, нужно позаботиться о предупреждении коллизий

Обработка выполнения команды на удаленном устройстве через ssh.

Собственно в консоли достаточно и такого варианта:

/system ssh address=10.20.0.1 user=root "ls"

В планировщике такое может не прокатить, поэтому используем ssh-exec:

/system ssh-exec address=10.20.0.1 user=root "ls"

Но как обработать полученный результат?, вообще ssh-exec с as-value возвращает именованный массив (массив с ключами) из двух элементов: "exit-code" и "output". Поэтому помещаем результат команды в переменную при помощи as-value:

:local AnswerServer ([/system ssh address=10.20.0.1 user=root "ls" as-value])

Вот теперь можно обработать результат выполнения команды:

:local AnswerServer "";
:set AnswerServer ([/system ssh address=10.20.0.1 user=root "ls" as-value])
:if ([:typeof $AnswerServer]="array" && [:len $AnswerServer]>="0") do={
	:if (($AnswerServer->"exit-code")="0") do={
		:log error ("Success to execute ssh command")
	} else={
		:log error ("Server respond exit-code: (" . ($AnswerServer->"exit-code") .")")
		:log error ("Server respond output: " . ($AnswerServer->"output"))
	}
} else={
	:log error "Server did not respond"
}

В этом коде я допустил опечатку, которая привела к ошибке cannot substract nothing from string

Первая же ошибка, из-за которой я и начал смотреть вывод, была "exit-code: 127" = "Нет такого файла или каталога". При этом команда snmpset, будучи запущенной через /system ssh не через шедуллер, отрабатывает на ура. 

Оказывается ошибка была в кавычках, преследуя цель сохранения синтаксиса - команда в кавычках, я выполнил подстановку кавычек с экранированием:

:set AnswerServer ([/system ssh-exec address=($settingSSH->"address") user=($settingSSH->"user") "\"$strSNMP\"" as-value])

Но, хоть вывод команды и не содержал ничего, предположение оказалось верным

:set AnswerServer ([/system ssh-exec address=($settingSSH->"address") user=($settingSSH->"user") $strSNMP as-value])

Не забываем удалять глобальные переменные

Продолжаем


Резюме

Сам скрипт не идеален:

  1. Нет проверки резолва FQDN имен
  2. Нет проверки доступности сетевых узлов
  3. Нет проверки наличия настроек
  4. Не сделан блок выключения серверов
  5. и прочее
Но на данном этапе он выполняет мои задачи, плюс прокачал некоторые навыки

Собственно детище:
# ищем среди глобальных переменных. ошибки здесь не будет, либо "", либо id
# в отличии от поиска через get, там и ошибки, и "не моментальное заполнение" списка,
# т.е. первый экзепляр висит уже 10  сек, а переменные через get еще отсутствуют
:if ([/system script environment find where name=chksmRunProgram]!="") do={
	:log error ("There is another instance of the program running, exit")
	:error message="There is another instance of the program running, exit"
}
# каждая глобальная переменная или функция должны начинаться с этого префикса, т.к. он будет использоваться при удалении глобальных переменных
:local pref "chksm"
:local countSMS 0

:global chksmRunProgram true
:global chksmWriteLog true
:global chksmphoneNumbersAdmin {"+79500146666";"+79117777537"}
:global chksmphoneNumbers ($chksmphoneNumbersAdmin,"+79116666666")
:global chksmArrayIdSMS [:toarray ""]
#Технически, можно опустить эту переменную и в коде и здесь, если в настройках микротика данные заполнены
:global chksmsettingMail [/tool e-mail print as-value]
:global chksmCodeEvents {"0"=" turned OFF the network!";"1"=" turned ON the network!";"ON"=" canceled shutdown servers!";"OFF"=" started shutdown servers!";"PING"=" contains code PING"}
:global chksmsettingSSH {"address"="srv1.domen.ru";"user"="user"}
# нужно выключить/включить порт/порты на нескольких аппаратах, поэтому такой страшный массив
:global chksmOIDandSettingsSNMP {{"settingSNMP"=({"address"="10.20.1.106";"1"="1";"0"="2";"community"="WriteCom"}); 
								"arrayOID"=({"1.3.6.1.2.1.2.2.1.7.27";"1.3.6.1.2.1.2.2.1.7.28"})}; 
							{"settingSNMP"=({"address"="10.20.1.101";"1"="1";"0"="2";"community"="WriteCom"}); 
								"arrayOID"=({"1.3.6.1.2.1.2.2.1.7.2"})}}



#   /\_______/\
# 
#   \_________/

# Функция логирования, нужна только для отладки. Ошибки в лог пишет всегда, а фот информативные сообщения только при WriteLog = true, задается выше
# $1 - тип записи в лог (error,info)
# $2 - текст сообщения
:global chksmLogging do={
	:global chksmWriteLog
	:local TextMessage ("SMSCheck => ".$2)
	:if ($chksmWriteLog) do={
		:if ($1="info") do={ :log info $TextMessage }
	}
	:if ($1="error") do={ :log error $TextMessage }
}

# Функция автоответа на кодовое слово
# $1 - телефонный номер
:local PONG do={
	:global chksmLogging
	do {/tool sms send lte1 message="PONG\n0 - DOWN\n1 - UP\nOFF - SHUTDOWN ALL\nON - SHUTDOWN STOP" phone-number=$1} on-error={
		$chksmLogging "error" ("SMS sending error (PONG) to $1")
	}
}

# Проверяем, входит ли номер в список разрешенных к управлению номеров
# $1 - телефонный номер
:local checkPhone do={
	:global chksmphoneNumbers
	# Перебираем разрешенные номера телефонов
	:foreach phoneNumber in=$chksmphoneNumbers do={
		:if ($phoneNumber=$1) do={:return (true)}
	}
	:return false
}

# Функция удаляет перевод строки из строки
# $1 - обрабатываемая строка
:local RemoveLineBreak do={
	:local textM $1
	:local textMnew "";
	:for i from=0 to=([:len $textM] - 1) do={
	   :local char [:pick $textM $i];
	   :if ($char != "\n") do={
		   :set textMnew ($textMnew . $char);
		 };
	};
	:return $textMnew
}
# Функция отправки писем
# $1 - Тема письма
# $2 - Тело письма
# $3 - true или опустить, тогда отправки не будет
:local SendMailSMS do={
	:global chksmsettingMail
	:global chksmphoneNumbersAdmin
	:global chksmLogging
	do {
		/tool e-mail send body=$3 subject=$2 from=($chksmsettingMail->"from") port=($chksmsettingMail->"port") server=($chksmsettingMail->"server") start-tls=($chksmsettingMail->"start-tls") to=($chksmsettingMail->"to")  
	} on-error={
		$chksmLogging "error" "E-Mail sending error"
	}
	:if ([:typeof $chksmphoneNumbersAdmin]="array" && [:len $chksmphoneNumbersAdmin]>="0" && $3=true) do={
		:foreach phoneNumber in=$chksmphoneNumbersAdmin do={
			do {
				/tool sms send lte1 message=$3 phone-number=$phoneNumber
			} on-error={
				# $chksmLogging "error" "SMS sending error ($3) to $phoneNumber"
			}
			# пачка смс не успевает уходить, надо обождать
			:delay 3000ms
		}
	}
}

# обрабатывает ответ сервера, вызывается примерно 3 раза
# $1 - ответ сервера
:global chksmCheckServerResponse do={
	:global chksmLogging
	:local AnswerServer $1
	:if ([:typeof $AnswerServer]="array" && [:len $AnswerServer]>="0") do={
		:if (($AnswerServer->"exit-code")="0") do={
			$chksmLogging "info" ("Success to execute ssh command")
		} else={
			$chksmLogging "error" ("Server respond exit-code: (" . ($AnswerServer->"exit-code") .")")
			$chksmLogging "error" ("Server respond output: " . ($AnswerServer->"output"))
		}
	} else={
		$chksmLogging "error" "Server did not respond"
	}
}
# Формирует строку команды для ssh
# $1 - новое состояние
# $2 - индекс строки массива OIDandSettingsSNMP
:global chksmGetSNMPString do={
	:global chksmOIDandSettingsSNMP
	:local settingSNMP ($chksmOIDandSettingsSNMP->"$2"->"settingSNMP")
	:local arrayOID ($chksmOIDandSettingsSNMP->"$2"->"arrayOID")
	:local strSNMP ""
	:local CodeEvent $1
	:if ([:typeof $settingSNMP]="array" && [:len $settingSNMP]>="0") do={
		:set strSNMP ("/usr/bin/snmpset -v2c -c " . ($settingSNMP->"community") . " " . ($settingSNMP->"address") . " ")
	} else={
		:return "false"
	}
	:if ([:typeof $arrayOID]="array" && [:len $arrayOID]>="0" ) do={
		:foreach OID in=$arrayOID do={
			:set strSNMP ($strSNMP . $OID . " i " . ($settingSNMP->"$1") . " ")
		}
		:return $strSNMP
	} else={
		:return "false"
	}
}
# Функцмя обработки событий
# $1 - код события
:local EventHandling do={
	#local var
	:local CodeEvent $1
	:local NumStr 0
	#global var
	:global chksmOIDandSettingsSNMP
	:global chksmsettingSSH
	#global func
	:global chksmLogging	
	:global chksmGetSNMPString
	:global chksmCheckServerResponse	
	
	:if ( ($CodeEvent="0") || ($CodeEvent="1") ) do={ 
		:if ([:typeof $chksmOIDandSettingsSNMP]="array" && [:len $chksmOIDandSettingsSNMP]>="0") do={
			:foreach strOIDandSettingsSNMP in=$chksmOIDandSettingsSNMP do={
				:local AnswerServer ""
				:local strSNMP [$chksmGetSNMPString $CodeEvent $NumStr]
				:if ($strSNMP!="false") do={
					$chksmLogging "info" ("Let's start changing state port switch ".($chksmOIDandSettingsSNMP->"$NumStr"->"settingSNMP"->"server"))
					$chksmLogging "info" ("system ssh address=" . ($chksmsettingSSH->"address") . " user=" . ($chksmsettingSSH->"user") . " " . $strSNMP)
					do {
						:set AnswerServer ([/system ssh-exec address=($chksmsettingSSH->"address") user=($chksmsettingSSH->"user") $strSNMP as-value])
					} on-error={
						$chksmLogging "error" ("Failed to execute ssh command (snmp)")
					}
					:set NumStr ($NumStr+1)
				} else={
					$chksmLogging "error" ("chksmGetSNMPString returned an empty response!")
				}
				$chksmCheckServerResponse $AnswerServer
			}
		}
	} 
	
	:if ( ($CodeEvent="OFF") || ($CodeEvent="ON") ) do={ 
		:put $CodeEvent
	}
}

#   /\__________________/\
# 
#   \____________________/

# Перебираем смски
:foreach message in=[/tool sms inbox print as-value] do={
	:local idSMS ($message->".id")
	:local phoneNumber ($message->"phone")
	:local TextMessage [$RemoveLineBreak ($message->"message")]
	:local OurPhone [$checkPhone $phoneNumber]
	
	:if ($OurPhone) do={
		:local bodyMessage ""
		# Кодовое слово для проверки доступности оборудования
		:if ($TextMessage="PING") do={ 
			:set bodyMessage ("Message from $phoneNumber " . ($chksmCodeEvents->$TextMessage))
			$chksmLogging "info" $bodyMessage
			$SendMailSMS $chksmsettingMail "INFO Code received" $bodyMessage
			$PONG $phoneNumber 
		} else={
			# Далее обработка рабочих кодов
			:set bodyMessage ($chksmCodeEvents->$TextMessage)
			:if ($bodyMessage != "") do={
				:set bodyMessage ("Number " . $phoneNumber . $bodyMessage)
				$chksmLogging "info" $bodyMessage
				$SendMailSMS $chksmsettingMail "ALARM Code received" $bodyMessage true
				$EventHandling $TextMessage
			}
			
		}
		# после обработки нужно удалить SMS, любую
		# уже не любую, удаляем только обработанные, все остальные PDU обработает и удалит
		# поэтому собираем id`ы в массив, иначе поломаем цикл
		:set $chksmArrayIdSMS ($chksmArrayIdSMS,{{$idSMS;$phoneNumber;$TextMessage}})
		:set countSMS ($countSMS+1)
	} 
}

# Блок удаления смс
:if ([:typeof $chksmArrayIdSMS]="array" && [:len $chksmArrayIdSMS]>="0") do={
	:foreach smsForRemove in=$chksmArrayIdSMS do={
		:local idSMS ($smsForRemove->0)
		:local phoneNumber ($smsForRemove->1)
		:local TextMessage ($smsForRemove->2)
		$chksmLogging "info" ("Let's start deleting SMS from $phoneNumber with text ($TextMessage)")
		do {
			# /tool sms inbox remove $idSMS
		} on-error={
			$chksmLogging "error" ("Error remove SMS from $phoneNumber")
		}
	}
}

# Пишем в лог факт обработки или не обработки смсок
:if ($countSMS>0) do={
	$chksmLogging "info" ("CheckSMS Script compleated. $countSMS messages parsed")
} else={
	$chksmLogging "info" ("CheckSMS Script compleated. No new messages")
}

# удаляем глобальные переменные
:foreach var in=[/system script environment print as-value] do={
	:local prefVar [:pick ($var->"name") 0 [:len $pref]]; 
	:if ($prefVar=$pref) do={
		/system script environment remove ($var->".id")
	}
}
:put "End programm"

Комментариев нет:

Отправить комментарий