Есть у меня сервер, с воткнутым в него свистком, который принимал смс. По крону смски проверялись и выполнялись определенные операции при поступлении управляющих кодов. Последнее время модем начал глючить, да и баланс уже год не показывает и сервер состарился, а тут еще и 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])
Не забываем удалять глобальные переменные
Продолжаем
Резюме
Сам скрипт не идеален:
- Нет проверки резолва FQDN имен
- Нет проверки доступности сетевых узлов
- Нет проверки наличия настроек
- Не сделан блок выключения серверов
- и прочее
# ищем среди глобальных переменных. ошибки здесь не будет, либо "", либо 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"})}}
:global chksmTo "it@domain.ru"
# /\_______/\
#
# \_________/
# Функция логирования, нужна только для отладки. Ошибки в лог пишет всегда, а фот информативные сообщения только при 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 chksmTo
:global chksmphoneNumbersAdmin
:global chksmLogging
do {
$chksmLogging "info" ("tool e-mail send body=" . $3 . " subject=" . $2. " from=" . ($chksmsettingMail->"from") . " port=" . ($chksmsettingMail->"port") ." server=" . ($chksmsettingMail->"address") . " start-tls=" . ($chksmsettingMail->"start-tls") . " to=" . $chksmTo)
/tool e-mail send body=$3 subject=$2 from=($chksmsettingMail->"from") port=($chksmsettingMail->"port") server=($chksmsettingMail->"address") start-tls=($chksmsettingMail->"start-tls") to=$chksmTo
} 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"
Комментариев нет:
Отправить комментарий