Печать

Зачем ещё один гайд про NUT?

Гайдов по подключению и настройке бесперебойников, то бишь, UPS-ов к компьютеру с linux-ом на борту - тьма тьмущая. И я ни за что не стал бы писать этот текст, если бы не один единственный, но крайне существенный нюанс. Практически все они (по крайней мере, из предложенных гуглом) предлагают такой вариант настройки, при котором компьютер будет выключаться, лишь получив от ups-а сигнал о низком уровне заряда батарей. К сожалению, практика показывает, что с Ippon-ами такое если и работает, то только на новом бесперебойнике с новыми аккумуляторами. Да и то весьма часто, если компьютер и успевает заподозрить что что-то с зарядом батареи не так - на корректное завершение работы времени уже не остаётся.
Логичным выходом будет отключать системник, если он проработал от бесперебойника какое-то фиксированное (гарантированно безопасное) время и питание к этому моменту не восстановилось. Родные "драйвера", идущие с Ippon-ами - это вполне себе умеют. Но вот беда - "драйвер" представляет из себя службу\GUI-морду на Java, и если для рабочего компьютера с иксами и каким-нибудь гномом оно ещё куда ни шло, то для сервера такой вариант - как то совсем не алё :(. К счастью, поскольку речь о linux - у нас тут unix-way, а значит обязательно должна быть программа которая, что называется, мало что умеет, но то что умеет - делает хорошо. И такая программа для управления ups-ами действительно существует. Существует она не одна, но для Ippon-ов из имеющихся вариантов (насколько мне известно) подходит только NUT, он же Network UPS Tools.
И вот тут то подкрадывается основная закавыка. NUT, конечно, замечательный инструмент - тут тебе и сервер, и клиент, и мониторинг, и возможность рулить хоть одним хоть сотней компьютеров, чего он только не умеет... При должном понимании принципов его работы - позволяет настроить сколь угодно сложные реакции на происходящие с бесперебойником события. Однако все эти сложные реакции пользователь должен написать самостоятельно (хоть на сях, хоть на баше, хоть на питонах-перлах - главное чтобы исполнимый файл мог запустить NUT-овский демон и передать ему аргументы), такие дела. Настроить только через конфиги такую простую вещь как "отключиться после N секунд работы на батареях" в NUT невозможно, вероятно поэтому подобная настройка и не предлагается ни в типовых примерах из оригинальной документации, ни в существующих русскоязычных гайдах. Нужный нам функционал даже в родной документации NUT скупо и лишь в общих чертах расписан в разделе Advanced usage (желающие покурить первоисточник могут пройти по ссылке и осознать всё "из первых рук").
Этот гайд я решил разделить на несколько логических частей.

Ну и последнее - в аббревиатуре NUT первая буква она не просто так - это действительно набор сетевых утилит, в первую очередь заточенный на управление большой кучей бесперебойников и питаемых ими компьютеров, объединённых в самых разных конфигурациях типа "1-2 бесперебойника на стойку из десятка серверов". Похоже, эта заточенность и объясняет отсутствие "из коробки" элементарного функционала, типичного для идущего в комплекте с ups-ами софта - у разработчиков голова болит о другом, а то что вместо истребителя при сборке получается паровоз - так в инструкции же написано - детали доработать напильником...

Что из себя представляют и как вообще работают Network UPS Tools.

В родной документации можно прочитать много-много умных букв, а также слов на эту тему, но нам важно знать, что в состав этого "сетевого набора утилит" входят следующие компоненты:

  1. сервер, питается картошкой %)
    Сервер, он же upsd. Как видно из названия - представляет из себя обычного демона (он же служба, он же сервис). Запускается он на том же компе, к которому физически (например по USB) подключён бесперебойник. В дебиане управляется скриптом /etc/init.d/nut-server. Настраивается конфигами: /etc/nut/upsd.conf, /etc/nut/upsd.users, /etc/nut/ups.conf. Но пути к конфигам NUT могут быть другими в другом дистрибутиве!
  2. Драйвер же!
    Драйверы. К ядру операционки они особого отношения не имеют, исполняются в юзерспейсе. Представляют из себя обычные исполнимые бинарники. Их много разных, лежат где-то приблизительно в /lib/nut, могут иметь какие-то свои специфические настройки (смотря для какого устройства и что это устройство умеет). Драйвер(ы) запускает upsd после того как почитает свои конфиги дабы получать через них информацию от ups-ов, после чего эти самые драйверы можно невозбранно увидеть в списке запущенных процессов. Конфиг читают из /etc/nut/ups.conf.
  3. монитор такой монитор
    Клиент, он же монитор, он же upsmon. Несмотря на свою клиентскую сущность - обычно тоже работает как демон (управляется скриптом /etc/init.d/nut-client). А вот этот товарищ запущен может быть решительно где угодно - можно даже на NAS или свой телефон на ведроиде его воткнуть, хотя в последнем смысла не многим больше чем в троллейбусе из буханки белого (или чёрного) хлеба. upsmon на androidИменно upsmon отвечает за то, чтобы тактически грамотно выключить компьютер, когда нет ни питания, ни надежды на его возобновление. Будучи клиентом - информацию о состоянии бесперебойника монитор получает, как нетрудно догадаться, от сервера, т.е. от upsd. Работать upsmon может в двух режимах: master - используется только на том же самом компьютере, к которому ups подключён управляющим проводом (например usb); либо в режиме slave - используется если компьютер питается от упса но не может им напрямую управлять. Отличие master от slave в общем-то только в том что master одновременно с завершением работы своей операционной системы отдаёт бесперебойнику команду отключить питание с нагрузки. Конфигурируется монитор через /etc/nut/upsmon.conf
  4. Типа таймер ))
    Таймер, он же upssched. Эта программка входит в комплект, однако изначально NUT не настроен на то чтобы её использовать, это просто один из возможных вариантов, который upsmon запускает в качестве реакции на те или иные события, т.е. вместо upssched в настройках можно указать, к примеру, самописный скрипт. Однако делать этого имхо не стоит т.к. во-первых самописный скрипт всё равно можно (и придётся!) настроить на запуск из upssched-а, а во вторых - основная фишка upssched-а в том что он умеет работать с таймерами. Получив от upsmon-а ту или иную команду, upssched может запустить отсчёт времени, вместо того чтобы просто сразу же передать её некоему третьему исполнимому файлу. И лишь когда созданный таким образом таймер дотикает до нуля - команда будет передана к примеру в настроенный нами самописный скрипт (обзовём его, скажем, upssched-cmd, хотя это и не принципиально). Есть также возможность прервать работу ещё не дотикавшего таймера по команде от upsmon-а (например, в качестве реакции на событие "вернулось питание"). Созданные upssched-ом таймеры являются отдельными процессами и видны в их списке, причём в upssched предусмотрена защита от "гонки" процессов, которая с небольшой вероятностью может возникнуть если upsmon, реагируя на множество событий, начнёт очень часто вызывать upssched. Конфиг с настройками - /etc/nut/upssched.conf.
  5. Стоит также сказать, что в debian-based-дистрибутивах скрипты инициализации читают файл /etc/nut.conf чтобы определить какие именно демоны вообще запускать. В других дистрибутивах вполне может быть что-то аналогичное - обратите внимание на документацию соответствующих пакетов и на то, что пишет пакетный менеджер при установке.

Подведём итог... и чтобы не писать опять много букв про всё то - же самое - сведу-ка я это в виде схемы:
Схема работы компонентов NUT

Как всё будет работать в итоге.

Да всё просто в общем то - скрипты инициализации запускают на одном и том же компьютере и upsd и upsmon, upsd запускает настроенный для имеющегося бесперебойника драйвер и начинает отслеживать его (бесперебойника) состояние. Upsmon подключается к upsd с указанными в настройках upsd именем и паролем пользователя, после чего тоже начинает внимательно наблюдать за состоянием бесперебойника. Как только бесперебойник оказывается в состоянии "питаюсь от батареи" - upsd узнаёт об этом от драйвера, а upsmon узнаёт уже от upsd. Upsmon смотрит свои настройки и видит - для состояния ONBATT он должен вызвать указанную в параметре NOTIFYCMD команду, передав ей через переменную окружения тип события - т.е., опять же, ONBATT. В NOTIFYCMD у нас указан upssched, который немедленно и запускается. Upssched смотрит уже свой собственный конфиг и видит, что получив событие типа ONBATT он должен запустить таймер (длительностью, к примеру, 60 секунд) с именем onbatt - ок, таймер запускается и начинает тикать. А теперь возможны варианты.

  1. Таймер может просто дотикать до нуля - тогда upssched берёт имя таймера, затем он берёт из своих настроек опцию CMDSCRIPT и запускает указанный в этой опции исполнимый файл (у нас будет upssched-cmd) с параметром, идентичным имени таймера. То есть он запустит upssched-cmd с параметром onbatt. Upssched-cmd в нашем случае будет самым обыкновенным bash-скриптом сcase-конструкцией, проверяющий переданный параметр и действующий в зависимости от этого самого параметра. Получив onbatt - скрипт во-первых ругнётся в логи (полезно для отладки), во-вторых отправит бесперебойнику команду shutdown.return (получив эту команду UPS подождёт ранее настроенное время и уберёт питание с нагрузки, а в случае восстановления питания - вернёт питание на нагрузку), в третьих - начнёт завершение работы операционной системы. Для отправки команды бесперебойнику мы воспользуемся командой upscmd - она входит в набор nut и тоже является клиентом для upsd т.е. подключается к нему по сети. Завершать работу операционки можно было бы, к примеру, просто командой halt, однако мы воспользуемся upsmon -c fsd - она во-первых заставляет upsmon передать сигнал fsd (forced shutdown) на upsd, что может быть важно если к upsd подключены upsmon с других, питающихся от того же UPS-а компьютеров, а во-вторых - выполняет собственно завершение работы операционки. По идее, работающий в режиме мастера upsmon при получении fsd должен ещё и обеспечивать передачу на UPS команды shutdown.return, однако "из коробки" оно в Debian на работает. Чтобы заработало - потребуется глубоко вникнуть в устройство скриптов, исполняющихся при завершении работы системы... короче гораздо проще отослать на UPS команду "руками", что и делает наш скрипт.
  2. Питание может вернуться до того как скрипт onbatt дотикает до нуля. Сигнал ONLINE при этом опять пойдёт по цепочке driver-upsd-upsmon-upssched. Последний, увидев в настройках что на ONLINE он должен остановить работу таймера onbatt - прибивает процесс таймера. Естественно таймер уже не дотикает и скрипт upssched-cmd вызван не будет.
  3. Чисто теоретически возможна ситуация, когда таймер ещё не дотикал, но от драйвера поступает сигнал LOWBATT - то есть бесперебойник уже на последнем издыхании. В этом случае upssched решает "резать, не дожидаясь перитонита", т.е. вызывает upssched-cmd с командой onbatt, не дожидаясь когда там дотикает таймер. В итоге компьютер попытается успеть выключиться до того как UPS окончательно сдохнет.

Теперь несколько слов о восстановлении питания. Что будет если питание вернётся когда upssched уже запустил обратный отсчёт времени, но завершение работы системы ещё не началось - понятно и уже расписано выше. Но обычно важно и чтобы сервер сам загрузился после того как электропитание будет восстановлено - для этого надо залезть в BIOS и найти там в разделе навроде "power management" опцию с названием что-то типа "AC Back Function". Называться и настраиваться оно может в общем то как угодно, особенно в BIOS-ах серверных материнок, главное смысл %). А смысл в том что системник включается сам сразу - как только появляется питание. На том собранном из хлама как-бы-сервере, где я последний раз настраивал работу бесперебойника, опция выглядит так:
опция в биосе
Если питание вернётся после того, как операционная система завершила работу, системник выключился, UPS тоже выключился (т.е. перестал подавать питание на нагрузку) - всё заработает и начнёт загружаться практически сразу. А вот если питание вернётся, к примеру, после того как операционка уже начала завершаться но системник ещё работает, либо после того как системник выключился но UPS ещё не отключался - тут возможны варианты. Совсем не факт что бесперебойник уберёт и потом вернёт питание по истечении настроенного для него времени если питание от сети уже вернулось - а в этом случае системник обратно не включится. Всё прямо зависит от того, как именно бесперебойник реагирует на восстановление питания после получения команды shutdown.return. В моём случае UPS действовал тактически грамотно во всех вариантах - т.е. после получения команды на отключение - он исправно ждал заказанные ему секунды, отключал питание с нагрузки, и только затем если питание от сети присутствовало - возвращал питание обратно. В идеале так и должно быть. Но вам стоит протестировать все возможные варианты, прежде чем оставлять сервер работать без присмотра. Иначе есть вероятность, что вас вытянут на работу утром в воскресенье потому что, например, сайт не работает. Если при тестировании выявились проблемы с загрузкой после восстановления питания, либо если просто очень важно чтобы сервер работал постоянно - возможно стоит настроить BIOS на включение, скажем, каждый день в 7 утра - если конечно в вашем биосе есть такая настройка.

А вот, собственно, и пошаговая инструкция по настройке. На примере Debian.

Первым делом устанавливаем пакет:

apt-get install nut

Команды, естественно, выполняем с правами суперпользователя, sudo если оно в вашей ситуации нужно - сами делаете :).
Не знаю как другие дистрибутивы, а Debian при установке пакета выдаст приблизительно следующее:

Настраивается пакет nut-client (2.6.4-2.3+deb7u1) …
[info] nut-client disabled, please adjust the configuration to your needs.
[info] Then set MODE to a suitable value in /etc/nut/nut.conf to enable it.
Настраивается пакет nut-server (2.6.4-2.3+deb7u1) …
[info] nut-server disabled, please adjust the configuration to your needs.
[info] Then set MODE to a suitable value in /etc/nut/nut.conf to enable it.

Это означает что пока мы не укажем режим работы NUT в конфиге /etc/nut/nut.conf - скрипты инициализации вообще не будут запускать демоны. Открываем этот конфиг в чём вы там конфиги редактируете. Я обычно редактирую в nano.

nano -w /etc/nut/nut.conf

Опция -w нужна чтобы nano не пытался разбивать длинные строки, что иногда может испортить конфиг.
В самом файле мы видим одну единственную опцию MODE, в комментариях же подробно расписано какие у этой опции могут быть значения. При этом нас вполне устроит режим standalone, его и прописываем.

MODE=standalone

Обратите также внимание что комментарий в конфиге крайне не рекомендует использовать пробелы вокруг символа равенства т.к. скрипты могут это дело неправильно понять.
Теперь нам надо определиться с тем, какой именно драйвер нужно использовать для бесперебойника. Во-первых, можно посмотреть таблицу совместимости на сайте NUT, на момент написания статьи она находилась по адресу http://www.networkupstools.org/stable-hcl.html. Во-вторых, примерно та же информация содержится в файле /usr/share/nut/driver.list (или аналогичном в других дистрибутивах). Так или иначе, различные Ippon-ы работают через драйвер blazer (blazer_ser для серийного порта и blazer_usb для, соответственно, usb). Посмотреть подробности по настройке этого драйвера можно либо командой man blazer, либо по ссылке на странице с сайта NUT - важно ознакомиться с документацией на выбранный драйвер, т.к. команды и настройки у каждого драйвера могут быть свои, уникальные. У меня по какой-то причине blazer_ser работать не захотел, а вот blazer_usb, несмотря на свой экспериментальный статус, работает как часы.

В общем, определяемся с драйвером, после чего лезем в файл /etc/nut/ups.conf. Вписываем в него примерно следующую секцию:

[ippon-smart-1000]
driver = blazer_usb
port = auto
desc = "Ippon Smart Power Pro 1000"
default.battery.voltage.high = 26.00
default.battery.voltage.low = 20.80
offdelay = 35
ondelay = 1

Итак, драйвер мы настроили. Теперь настраиваем основной демон. Лезем в конфиг /etc/nut/upsd.conf. В принципе, достаточно будет раскомментировать следующую опцию:

LISTEN 127.0.0.1 3493

Демон будет слушать 3493 порт на loopback-интерфейсе. Параноики могут явно закрыть извне 3493-й порт в iptables, но по идее из сети добраться до демона при такой настройке никто не сможет.
Теперь надо указать логин и пароль для подключения к демону - открываем /etc/nut/upsd.users. Комментарии не трогаем (желательно - читаем), вписываем в конце файла примерно такую секцию:

[nutuser]
password = nutpass
upsmon master
actions = SET
actions = FSD
instcmds = ALL

В квадратных скобках указан логин, параметром password - пароль. Строчка upsmon master означает, что подключившийся монитор будет работать в режиме мастера. Actions = SET - разрешает монитору менять параметры бесперебойника "на лету". Actions = FSD разрешает включать Forced Shotdown. Instcmds = ALL - разрешает с этого имени пользователя отдавать бесперебойнику любые поддерживаемые драйвером команды.

По идее, сейчас upsd уже должен уметь запускать драйвер и отслеживать состояние бесперебойника - запускаем демона:

/etc/init.d/nut-server start

Пробуем получить сведения от UPS-а:

upsc ippon-smart-1000@localhost

В ответ должно выдать что-то типа такого:

battery.charge: 100
battery.voltage: 27.80
battery.voltage.high: 26.00
battery.voltage.low: 20.80
battery.voltage.nominal: 24.0
device.type: ups
driver.name: blazer_usb
driver.parameter.offdelay: 35
driver.parameter.ondelay: 1
driver.parameter.pollinterval: 2
driver.parameter.port: auto
driver.version: 2.6.4
driver.version.internal: 0.08
input.current.nominal: 4.0
input.frequency: 50.1
input.frequency.nominal: 50
input.voltage: 226.2
input.voltage.fault: 226.2
input.voltage.nominal: 220
output.voltage: 223.9
ups.beeper.status: enabled
ups.delay.shutdown: 30
ups.delay.start: 60
ups.load: 11
ups.productid: 5161
ups.status: OL
ups.temperature: 25.0
ups.type: offline / line interactive
ups.vendorid: 0665

Если это действительно так - всё ок, драйвер и демон работают. Важно чтобы параметр ups.status имел значение OL т.е. OnLine - если оно отличается - что-то не так либо с драйвером, либо с самим устройством. Если же выдало ошибку, а в логах можно найти сообщения о том, что драйвер не может найти устройство - вероятно на бесперебойник не сработали правила udev. В таком случае вам нужно скопировать файл /lib/udev/rules.d/52-nut-usbups.rules в /etc/udev/rules.d/ - после чего либо подключить\отключить бесперебойник от компьютера (в случае USB), либо перезагрузиться, либо "дёрнуть" udev следующим образом:

udevadm control --reload-rules
udevadm trigger

После чего (если не перезагружались) - перезапустить сервис upsd и заново попробовать получить информацию от бесперебойника:

/etc/init.d/nut-server restart
upsc ippon-smart-1000@localhost

Надеюсь, всё заработало, если нет - рекомендации дать уже сложно, надо вдумчиво курить логи.

Будем считать что, всё работает и продолжим. Настраиваем монитор. Файл там довольно длинный, с кучей комментариев. Поэтому чтобы не выискивать нужные опции - лучше переименуйте /etc/nut/upsmon.conf в, к примеру, /etc/nut/upsmon.conf.old и создайте новый файл /etc/nut/upsmon.conf. В этот пустой файл вставляем что-то типа такого:

MONITOR ippon-smart-1000@localhost 1 nutuser nutpass master
MINSUPPLIES 1
NOTIFYCMD /sbin/upssched
SHUTDOWNCMD "/sbin/shutdown -Ph +0"
POLLFREQ 5
POLLFREQALERT 5
HOSTSYNC 15
DEADTIME 15
POWERDOWNFLAG /etc/nut/killpower
NOTIFYMSG ONLINE     "UPS %s on line power"
NOTIFYMSG ONBATT     "UPS %s on battery"
NOTIFYMSG LOWBATT    "UPS %s battery is low"
NOTIFYMSG FSD        "UPS %s: forced shutdown in progress"
NOTIFYMSG COMMOK     "Communications with UPS %s established"
NOTIFYMSG COMMBAD    "Communications with UPS %s lost"
NOTIFYMSG SHUTDOWN   "Auto logout and shutdown proceeding"
NOTIFYMSG REPLBATT   "UPS %s battery needs to be replaced"
NOTIFYMSG NOCOMM     "UPS %s is unavailable"
NOTIFYMSG NOPARENT   "upsmon parent process died - shutdown impossible"
NOTIFYFLAG ONLINE    SYSLOG+WALL+EXEC
NOTIFYFLAG ONBATT    SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT   SYSLOG+WALL+EXEC
NOTIFYFLAG FSD       SYSLOG+WALL+EXEC
NOTIFYFLAG COMMOK    SYSLOG+WALL+EXEC
NOTIFYFLAG COMMBAD   SYSLOG+WALL+EXEC
NOTIFYFLAG SHUTDOWN  SYSLOG+WALL+EXEC
NOTIFYFLAG REPLBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG NOCOMM    SYSLOG+WALL+EXEC
NOTIFYFLAG NOPARENT  SYSLOG+WALL+EXEC
RBWARNTIME 43200
NOCOMMWARNTIME 300
FINALDELAY 5

Теперь настроим upssched. Открываем /etc/nut/upssched.conf. Опять же, либо чистим файл, либо ищем там нужные незакомментированные параметры и проставляем там нужные нам значения. Получиться должно примерно что-то такое:

#Скрипт, который будет запускаться по завершению работы таймера или по EXECUTE
CMDSCRIPT /bin/upssched-cmd
# Именованный канал, через который upssched общается с процессами-таймерами.
PIPEFN /var/run/nut/upssched.pipe
# Блокировочный файл - нужен чтобы не было гонок процессов.
LOCKFN /var/run/nut/upssched.lock
#Если переходим на батареи - ждём 60 секунд и посылаем команду onbatt, которая вырубит сервер.
AT ONBATT * START-TIMER onbatt 60
#Если вернулось питание - отменить таймер для команды onbatt
AT ONLINE * CANCEL-TIMER onbatt
#Если батарейка села нафиг - сразу послать onbatt чтобы вырубить сервер.
AT LOWBATT * EXECUTE onbatt

Суть настроек уже объяснена в комментариях, хочу только заметить, что по-умолчанию в конфиге предлагаются другие значения PIPEFN и LOCKFN, и если их не менять - придётся (по крайней мере на Debian) идти на дополнительные ухищрения чтобы процессы, работающие от имени пользователя nut могли писать\читать\создавать эти файлы. А вот к каталогу /var/run/nut/ доступ у этих процессов обычно есть. В любом случае стоит убедиться что пользователь, от имени которого в вашем дистрибутиве работают демоны NUT имеет возможность создавать файлы, указанные в этих настройках.
Строки начинающиеся на AT - добавляют реакции на типы событий - для ONBATT (на батарейке) мы запускаем таймер на 60 секунд, для ONLINE - прерываем таймер, для LOWBATT - сразу выключаем компьютер.

Осталось только написать или отредактировать (если он уже существует) скрипт /bin/upssched-cmd. Убедитесь что файлу проставлены права на исполнение и что пользователь nut сможет запустить файл! Рекомендую просто заменить его содержимое на следующий текст:

#! /bin/sh
#
# This script should be called by upssched via the CMDSCRIPT directive.
#
# Here is a quick example to show how to handle a bunch of possible
# timer names with the help of the case structure.
#
# This script may be replaced with another program without harm.
#
# The first argument passed to your CMDSCRIPT is the name of the timer
# from your AT lines.

case "$1" in
        onbatt)
                logger -t upssched-cmd "60 seconds on battary - halt system and poweroff UPS after 35 sec"
                upscmd -unutuser -pnutpass ippon-smart-1000@localhost shutdown.return
                sudo upsmon -c fsd
                ;;
        upsgone)
                logger -t upssched-cmd "The UPS has been gone for awhile"
                ;;
        *)
                logger -t upssched-cmd "Unrecognized command: $1"
                ;;
esac

Важный момент. Процесс upssched скорее всего будет работать от имени пользователя nut. И, опять же, скорее всего этот пользователь не имеет права завершать работу операционной системы. Я обошёл это добавлением sudo к вызову команды upsmon -c fsd и добавлением в /etc/sudoers следующих строчек:

#allow shutdown for nut
nut ALL=NOPASSWD: /sbin/shutdown, /sbin/upsmon -c fsd

Но есть и другие варианты, например добавлением пользователя nut в соответствующую группу - зависит от вашего дистрибутива.

Вот и всё, можно (если ещё не) запускать сервисы NUT (либо просто перезагрузиться) и приступать к тестированию работоспособности всей этой конструкции.
В процессе тестирования сообщения от NUT будут падать в /var/log/syslog - отслеживать изменения в этом логе удобно такой командой:

watch -n 0,5 tail -n 50 /var/log/syslog

Вот вроде и всё, спасибо что прочитали.

Если вы заметили ошибки\неточности - не стесняйтесь отписаться о них в комментариях.


 


 

www.38i.ru