Користувальницькькі налаштування

Налаштування сайту


Сайдбар

Розділи

Загальний опис
Історія змін
Рекомендації щодо оновлення
Плани на майбутнє
Відомі проблеми
Онлайн демо
Допомога проєкту
Люди
Трохи про безпеку

FAQ



Редагувати сайдбар

watchdog

Собака-спостерігака aka Watchdog

Собака-спостерігака призначена для собачення та спостерігачення оперативного моніторингу навколишньої реальності. Вона надає гнучкий функціонал для опису позаштатних ситуацій практично будь-якої дивності, а також для сповіщення у разі їх виникнення. У тому числі, за допомогою відсилання SMS через послуги, що підтримуються собакою-посилакою, електронною поштою, месенджера Telegram а також може викликати запуск будь-якого зовнішнього скрипта при настанні якоїсь події. Для ввімкнення собаки-спостерігаки, потрібна зміна опції WATCHDOG_ENABLED в alter.ini. Також для надсилання повідомлень, очевидно, на допомогу собаці-спостерігаці знадобиться Собака-посилака.

Можливі перевірки

Типи задач Дія Повертає
icmpping виконується ICMP ping хоста вказаного в параметрі bool
tcpping виконується спроба TCP з'єднання з хостом вказаним в параметрі у вигляді host:port bool
udpping виконується спроба UDP з'єднання з хостом вказаним в параметрі у вигляді host:port bool
hopeping Пінг надії. Тричі виконується ICMP ping хоста вказаного в параметрі, в надії, що хоч один з них повернеться bool
script запуск shell-скрипту за шляхом, вказаному в параметрі string
httpget отримання сирих даних з URL вказаного в параметрі string
getusertraff отримання кількості трафіку в байтах логіну користувача вказаного в параметрі int
fileexists перевірка на існування файлу по шляху вказаному в параметрі bool
opentickets кількість відкритих тікетів хелпдеску. Потребує вказання рандомного параметра int
onepunch Виконує запуск One-Punch скрипта з аліасом вказаним в параметрі. Результат очікується у вигляді змінної $watchdogCallbackResult string
snmpwalk Виконується snmpwalk по OID хоста що вказано в параметрі у форматі host:community:OID string
freediskspace Повертає кількість вільного місця на розділі (точці монтування) вказаній в параметрі. Повертає цифру в Гб. float

Можливі оператори для перевірок

Оператор Значення Потребує “Умову”?
=true Істинно
=false Хибно
== Рівне +
!= Не рівне +
> Більше +
< Менше +
> = Більше або рівне +
< = Менше або рівне +
empty Пустий результат
notempty Непустий результат
changed Змінилось
notchanged Не змінилось
like Містить +
notlike Не містить +
rised Збільшилось +-
decreased Зменшилось +-

Дії що будуть виконані у разі проходження умов

Дії Результат
log запис події в системний лог
sms надсилання SMS сповіщення на номери стільникових, вказаних в налаштуваннях Собаки-спостергаки. Додаткові номери стільникових можна вказати у форматі {номер,номер}.
noprimary у випадку наявності цієї дії, та дії sms та вказаних {додаткових номерах} - основні номери з налаштувань Собаки-спостерігаки будуть проігноровані.
email надсилання сповіщення електропоштою, на адреси вказані в налаштуваннях.
telegram надсилання повідомлення Telegram, додаткові chatid можна вказати в форматі (чат1,чат2).
no_tg_primary у випадку наявності цієї дії, та дії telegram та вказаних (додаткових чатах) - основні чати Telegram ігноруються.
andresult у разі вказаних дій sms, telegram чи email до тексту повідомлення буде додано поточний результат завдання
oldresult у разі вказаних дій sms, telegram чи email до тексту повідомлення буде додано попередній результат завдання
script запуск скрипта чи будь-якого додатку, за шляхом, вказаному у вигляді [/повний/шлях/до_скрипта]

Логіка роботи

Кожне завдання для Собаки-спостерігаки слід сприймати як “щось трапилося” або “ой яка подія” які трапляються у разі повернення “типом перевірки” за “параметром” результату передбаченого “оператором” з опціональною “умовою”. Як приклад, можна навести ось таке просте завдання:

Ім`я Тип перевірки Параметр Оператор Умова Дії
Гугл не пінгається icmpping google.com =false log,sms,email

При настанні події, коли ping на адресу google.com поверне значення “хибно” собака-спостерігака стурбовано надішле вам СМС-ку, почту та запише в лог сповіщення, про те, що “Гугл не пінгається”. Власне і продовжить це робити, допоки перевірка icmpping не перестане повертати значення “хибно”.

А що робити, якщо ми не хочемо, щоб собака постійно нам спамила за відсутнього пінгу кудись? А дуже просто.

Ім`я Тип перевірки Параметр Оператор Умова Дії
Щось змінилось icmpping 192.168.0.22 changed log,sms,email,andresult

В принципі, ніхто не забороняє нам робити і завдання такого плану:

Ім`я Тип перевірки Параметр Оператор Умова Дії
В серверній пожежа script /bin/gettemp > 22 log,sms,email, andresult

Викликаючи зовнішній скрипт, що знімає температуру з термодатчиків і при перевищенні 22 градусів кричати всіма відомими способами також дописуючи в повідомленні про температуру що викликає паніку.

Якщо творчо підійти до парсингу виведення зовнішнього ПЗ - можна моніторити багато цікавих речей без дописування зовнішніх скриптів:

Ім`я Тип перевірки Параметр Оператор Умова Дії
DNS зламався script nslookup google.com | tail -n 2 like find log,sms

Також ми можемо дуже просто та елегантно контролювати запущеність важливих сервісів типу stargazer, створивши завдання такого плану:

Ім`я Тип перевірки Параметр Оператор Умова Дії
Stargazer впав script /bin/ps aux | /usr/bin/grep stg notlike stargazer log,sms

Хоча те саме завдання ми можемо оформити як

Ім`я Тип перевірки Параметр Оператор Умова Дії
Stargazer впав fileexists /var/run/stargazer.pid =false log,sms

Щоб не зосереджуватись на теоретичних речах, ми можемо передбачити ситуацію, коли у нас є дуже важливий клієнт на падіння якого, нам слід таки відреагувати. Описати таке ми можемо у вигляді:

Ім`я Тип перевірки Параметр Оператор Умова Дії
Важливий клієнт подох hopeping 172.16.78.42 changed log,sms andresult

Окей, а якщо ми хочемо також цю ж СМС послати скажемо адміністратору цього ж важливого клієнта, щоб він точно знав, що він здох?

Ім`я Тип перевірки Параметр Оператор Умова Дії
Важливий клієнт подох hopeping 172.16.78.42 changed log,sms andresult {+380509999999}

а якщо ми хочемо надіслати СМС лише на додатковий номер? Так, жодних проблем:

log,sms {+380509999999} noprimary

а якщо ми хочемо надіслати алерт тільки в додатковий чат Telegram? Так, так само без проблем:

log,telegram,email (ChatID1,ChatID2,ChatID3) no_tg_primary

а можна міксувати СМС та Telegram? Так, просто вказуємо в діях все що нам потрібно разом:

log,telegram,sms,email (ChatID1,ChatID2,ChatID3) no_tg_primary {+380509999999} noprimary

Хоча знову ж таки ніхто не забороняє нам, скажімо моніторити цього клієнта наприклад зовнішнім скриптом, що запитує по SNMP стан порту або лічильники трафіку на світі, куди його встромлено, або банально його смикати по якомусь TCP порту. Думаю вищенаведених прикладів достатньо для отримання уявлення про гнучкість собаки-спостерігаки та можливості побудови сповіщень при практично, будь-яких позаштатних ситуаціях. Обробка завдань собаки відбувається при виклику watchdog з RemoteAPI. Інтервалом, що рекомендується, є 10 хвилин. В crontab це виглядає наступним чином:

*/10 * * * *   /bin/ubapi "watchdog"

А ще ми можемо реагувати не лише на зміни поточних значень щодо якихось порогів, а також і відносно попередніх значень, отриманих собакою. Різкість цих змін ми можемо опціонально вказувати в “умові”. А можемо й не вказувати. Тоді ми реагуватимемо взагалі на всі зміни цих циферок у бік збільшення або зменшення. Наприклад, ми можемо реагувати на збільшення зростання помилок на якомусь інтерфейсі.

Ім`я Тип перевірки Параметр Оператор Умова Дії
Помилки полізли onepunch uplinkerrors rised log,sms,telegram

Ну або якийсь рівень зростання помилок ми вважаємо припустимим, і наприклад, встановлюємо поріг у 100 помилок за 10 хвилин

Ім`я Тип перевірки Параметр Оператор Умова Дії
Помилки полізли активно onepunch uplinkerrors rised 100 log,sms,telegram

Також ми можемо у такий спосіб відловлювати або різкі сплески або падіння утилізації, наприклад, того ж каналу. Типу, ми вважаємо, що якщо утилізація каналу за останні 10 хвилин зросла на вісім гіг.. типу щось пішло не так

Ім`я Тип перевірки Параметр Оператор Умова Дії
Канал якось розігнався onepunch uplinktraffic rised 8000 log,sms,telegram

Ну чи навпаки різкі падіння щодо попередніх значень (типу утилізація аплінку провалилася на 20 гіг від останнього запуску собаки)

Ім`я Тип перевірки Параметр Оператор Умова Дії
Трафік рухнов якось сильно onepunch uplinktraffic decreased 20000 log,sms,telegram

А ще ми можемо дуже просто перевіряти робочість сервісів, які повинні слухати з'єднання на якісь TCP або UDP порти, типу так

Ім`я Тип перевірки Параметр Оператор Умова Дії
http на хості tcpping 192.168.42.18:80 changed log,sms,telegram andresult
Ім`я Тип перевірки Параметр Оператор Умова Дії
https на хості tcpping 192.168.42.18:443 changed log,sms,telegram andresult
Ім`я Тип перевірки Параметр Оператор Умова Дії
syslogd на хості udpping 192.168.42.18:514 changed log,sms,telegram andresult

А ще ми можемо отримувати та перевіряти будь-які дані з будь-якого OID за допомогою snmpwalk:

Ім`я Тип перевірки Параметр Оператор Умова Дії
Версія OS змінилась snmpwalk 192.168.42.18:changeme:.1.3.6.1.2.1.1.1.0 changed log,sms,telegram andresult

Або банально нотифікувати себе про те, що у кореневому розділі закінчується місце

Ім`я Тип перевірки Параметр Оператор Умова Дії
В корені закінчується місце freediskspace / < 100 log,sms,telegram andresult

Відправка SMS

Весь функціонал відправлення реалізований за допомогою підсистеми "Собака-посилака". Номери для відсилання розділяються комами та вказуються у міжнародному форматі (наприклад +380509999999) в налаштуваннях “Собаки-спостерігаки”. Їх як основних так і додаткових може бути скільки завгодно.

Відсилка Telegram

Здійснюється також під час проходу “Собаки-посилаки”. Як налаштувати свого бота для розсилки, можна почитати тут.

Приклади скриптів

Як вже сказано вище, за допомогою собаки-спостерігаки, можливо заскриптувати і контролювати взагалі все, що завгодно - це обмежено тільки вашою фантазією та радіусом кривизни рук. Ми рекомендуємо зберігати кастомні скрипти такого плану в content/documents/myscripts/ - таким чином вони нормально переживатимуть оновлення. Ось кілька простих прикладів:

Отримання трафіку з порта комутатора

switch_traffic
#!/usr/local/bin/php
<?php
 
//config section
$port='24';
$oid='.1.3.6.1.2.1.31.1.1.1.6';
$ip='192.168.0.15';
$community='yourcommunity';
//end of config
 
$cmd='/usr/local/bin/snmpwalk -v2c -On  -c '.$community.' '.$ip.' '.$oid.'.'.$port;
$raw=shell_exec($cmd);
$newTime=time();
if (!empty($raw)) {
$raw=explode('Counter64:',$raw);
$raw=trim($raw[1]);
if (!empty($raw)) {
        $cacheName=dirname(__FILE__).'/octets_'.$ip.'_'.$port;
        if (file_exists($cacheName)) {
                $oldTime=filemtime($cacheName);
                $oldOctets=file_get_contents($cacheName);
                $traffDiff=$raw-$oldOctets;
                $timeDiff=$newTime-$oldTime;
                if ($timeDiff!=0) {
                $speed=($traffDiff*8*100)/($timeDiff*10000);
                print(round($speed/10000));
                } else {
                print('-1');
                }
            file_put_contents($cacheName,$raw);
        } else {
                file_put_contents($cacheName,$raw);
                print('0');
        }
}
 
}

Зняття температури з ping3

eping_temp
#!/usr/local/bin/php
<?php
 
$ip='192.168.0.89';
$community='yourcommunity';
$oid='1.3.6.1.4.1.35160.1.16.1.13.1';
$result=0;
 
 
$command='/usr/local/bin/snmpwalk -On -r 1 -t 1 -v2c -c '.$community.' '.$ip.' '.$oid;
$resultRaw=shell_exec($command);
        if (!empty($resultRaw)) {
                $result=explode('INTEGER:',$resultRaw);
                $result=(isset($result[1])) ? $result[1] : 0;
                $result=$result/10;
        }
 
print($result);

Наявність електомережі з ping3

eping_power
#!/usr/local/bin/php
<?php
 
$ip='192.168.0.89';
$community='yourcommunity';
$oid='1.3.6.1.4.1.35160.1.26.0';
$result=0;
 
 
$command='/usr/local/bin/snmpwalk -On -r 1 -t 1 -v2c -c '.$community.' '.$ip.' '.$oid;
$resultRaw=shell_exec($command);
        if (!empty($resultRaw)) {
                $result=explode('INTEGER:',$resultRaw);
                $result=(isset($result[1])) ? $result[1] : 0;
                $result=($result==1) ? 'OK' : 'FAILED!';
        }
 
print($result);

Кількість вільного місця в /var/

var_stat
#!/usr/local/bin/php
<?php
$result=disk_free_space("/var/");
$result=$result/1024/1024/1024;
$result=round($result,2);
 
print($result);
?>

Джиттер до гугла

check_icmp_google
#!/bin/sh
ping -c 1 8.8.8.8 | head -n 2 | tail -n 1 | awk -F "=" '{print $4}' | awk -F " " '{print $1}

Хешрейт на ethermine.org

ethereum_hashrate
#!/usr/local/bin/php
<?php
 
$wallet='72F7B047C19217871cef591Bc6e960aAD333B822';
$workersStatsUrl = 'https://api.ethermine.org/miner/:'.$wallet.'/workers';
$jsonRaw = file_get_contents($workersStatsUrl);
$reportedHashrate = 0;
 
if (!empty($jsonRaw)) {
    $workersStats = json_decode($jsonRaw, true);
    if (!empty($workersStats)) {
        foreach ($workersStats['data'] as $io => $each) {
                $reportedHashrate+=$each['reportedHashrate'];
        }
    }
}
 
print(round($reportedHashrate/1000000,2));

PPS на інтерфейсі

stat_pps
#!/bin/sh
/usr/bin/netstat -w 1 -I bridge0  -q 1 | /usr/bin/tail -n 1 | /usr/bin/awk {'print $1'}

Утилізація CPU Linux хоста

lin_cpustats
#!/usr/local/bin/php
<?php
 
/**
CPU Statistics
 
Load
1 minute Load: .1.3.6.1.4.1.2021.10.1.3.1
5 minute Load: .1.3.6.1.4.1.2021.10.1.3.2
15 minute Load: .1.3.6.1.4.1.2021.10.1.3.3
 
CPU
percentage of user CPU time: .1.3.6.1.4.1.2021.11.9.0
raw user cpu time: .1.3.6.1.4.1.2021.11.50.0
percentages of system CPU time: .1.3.6.1.4.1.2021.11.10.0
raw system cpu time: .1.3.6.1.4.1.2021.11.52.0
percentages of idle CPU time: .1.3.6.1.4.1.2021.11.11.0
raw idle cpu time: .1.3.6.1.4.1.2021.11.53.0
raw nice cpu time: .1.3.6.1.4.1.2021.11.51.0
 
*/
$oid='.1.3.6.1.4.1.2021.10.1.3.3';
$ip='192.168.0.70';
$community='yourcommunity';
//end of config
 
$cmd='/usr/local/bin/snmpwalk -v2c -On  -c '.$community.' '.$ip.' '.$oid;
$raw=shell_exec($cmd);
$newTime=time();
if (!empty($raw)) {
$raw=explode('STRING:',$raw);
$raw=trim($raw[1]);
if (!empty($raw)) {
    print($raw);
} else {
    print('FAIL');
}
 
}

Використання One-Punch скриптів

Ще більш доцільним є використання One-Punch скриптів замість просто скриптів, котрі лежать десь на вашій ФС. Використовуючи їх, ви отримуєте одразу дві головні переваги:

  1. Вони зберігаються у вашій БД і переїжджають завжди разом із нею
  2. Вони виконуються всередині Ubilling і мають прямий доступ до всього його функціоналу.

Як це працює?

Допустимо беремо і створюємо One-Punch скрипт наступного виду:

$watchdogCallbackResult='sometest data';

Ось якось так

І припустимо ми хочемо контролювати чи не зміняться дані, що повертаються цим скриптом (з чого б це? ;)

Сподіваюсь очевидно, що собака-спостерігака сприйматиме як результат виконання скрипта лише дані, що знаходяться в змінній $watchdogCallbackResult?


Також є ще одне невелике обмеження, що стосується не тільки даних, що повертаються One-Punch скриптами, а також і до таких типів перевірок як script і httpget. Обмеження полягає в тому, що оператори changed і notchanged не працюють адекватно якщо обсяг даних, що повертаються цими перевірками, становить більше 255 байт. Тому якщо ви збираєтеся використовувати ці оператори для контролю змін у даних, що повертаються вищевказаними типами, вам слід це враховувати при розробці ваших скриптів. Для перевірок типу like або скажемо notempty це не важливо.

Трафік з порта

Ось наприклад те саме зняття даних про трафік з порту світча, але вже у вигляді One-Punch скрипта:

//config section
$port='8';
$oid='.1.3.6.1.2.1.31.1.1.1.6';
$ip='192.168.18.234';
$community='changeme';
//end of config
 
$cmd='/usr/local/bin/snmpwalk -v2c -On  -c '.$community.' '.$ip.' '.$oid.'.'.$port;
$raw=shell_exec($cmd);
$newTime=time();
if (!empty($raw)) {
$raw=explode('Counter64:',$raw);
$raw=trim($raw[1]);
if (!empty($raw)) {
    $cacheName='content/documents/myscripts/octets_'.$ip.'_'.$port;
    if (file_exists($cacheName)) {
        $oldTime=filemtime($cacheName);
        $oldOctets=file_get_contents($cacheName);
        $traffDiff=$raw-$oldOctets;
        $timeDiff=$newTime-$oldTime;
        if ($timeDiff!=0) {
        $speed=($traffDiff*8*100)/($timeDiff*10000);
        $watchdogCallbackResult=round($speed/10000);
        } else {
        $watchdogCallbackResult='-1';
        }
        file_put_contents($cacheName,$raw);
    } else {
        file_put_contents($cacheName,$raw);
        $watchdogCallbackResult='0';
    }
}
 
}

Вільне місце на диску

$result=disk_free_space("/");
$result=$result/1024/1024/1024;
$watchdogCallbackResult=round($result,2);

Load Average віддаленого Linux хосту

$oid = '.1.3.6.1.4.1.2021.10.1.3.3';
$ip = '192.168.0.70';
$community = 'yoursnmpcommunity';
 
 
$cmd = '/usr/local/bin/snmpwalk -v2c -On  -c ' . $community . ' ' . $ip . ' ' . $oid;
$raw = shell_exec($cmd);
$watchdogCallbackResult = '';
if (!empty($raw)) {
    $raw = explode('STRING:', $raw);
    $raw = trim($raw[1]);
    if (!empty($raw)) {
        $watchdogCallbackResult .= $raw;
    }
}

Моніторинг isc-dhcpd

$command='ps aux | grep dhcpd | grep -v grep';
$result=shell_exec($command);
if (!empty($result)) {
  $watchdogCallbackResult='запущено';
} else {
  $watchdogCallbackResult='впав нахрін';
}

Моніторинг TrassirServer

        $watchdogCallbackResult = '';
        $dvrs = new NyanORM('visor_dvrs');
        $dvrs->where('type', '=', 'trassir');
        $allDvrs = $dvrs->getAll();
 
        if (!empty($allDvrs)) {
            foreach ($allDvrs as $io => $each) {
                $dvrName = $each['name'];
                $trassir = new TrassirServer($each['ip'], $each['login'], $each['password'], $each['apikey'], $each['port']);
                $health = $trassir->getHealth();
                if (!$health['disks'] OR ! $health['database'] OR ! $health['network']) {
                    $watchdogCallbackResult .= $dvrName . ' - FAILS ';
                }
            }
        }

Моніторинг температури з Equicom PING3

    $ip = '192.168.0.89';
    $community = 'yourcommunity';
    $oid = '1.3.6.1.4.1.35160.1.16.1.13.1';
    $correction = 0;
 
 
    $snmp = new SNMPHelper();
    $resultRaw = $snmp->walk($ip, $community, $oid, false);
    $watchdogCallbackResult = 0;
    if (!empty($resultRaw)) {
        $watchdogCallbackResult = zb_SanitizeSNMPValue($resultRaw) / 10;
        $watchdogCallbackResult = $watchdogCallbackResult + $correction;
    }

Моніторинг наявності живлення на Equicom PING3

    $ip = '192.168.0.89';
    $community = 'yourcommunity';
    $oid = '1.3.6.1.4.1.35160.1.26.0';
    $result = 0;
 
 
    $snmp = new SNMPHelper();
    $resultRaw = $snmp->walk($ip, $community, $oid, false);
    $watchdogCallbackResult = 0;
    if (!empty($resultRaw)) {
        $watchdogCallbackResult = zb_SanitizeSNMPValue($resultRaw);
        $watchdogCallbackResult = ($watchdogCallbackResult == 1) ? 'OK' : 'FAILED!';
    }

Моніторинг температур на OLT-ах

    $criticalTemp = 70;
 
    $watchdogCallbackResult = '';
    $tempPath = OLTAttractor::TEMPERATURE_PATH;
    $tempExt = OLTAttractor::TEMPERATURE_EXT;
 
    $switchesDb = new nya_switches();
    $switchesDb->where('desc', 'LIKE', '%OLT%');
    $allOlt = $switchesDb->getAll();
 
    foreach ($allOlt as $io => $eachOltData) {
        $tempData = $tempPath . $eachOltData['id'] .'_'. $tempExt;
        if (file_exists($tempData)) {
            $oltTemp = file_get_contents($tempData);
            if ($oltTemp >= $criticalTemp) {
                $watchdogCallbackResult .= $eachOltData['location'] . ' - ' . $oltTemp . ' °C ';
            }
        }
    }
 
    if (empty($watchdogCallbackResult)) {
        $watchdogCallbackResult='Всі температури OLT в порядку';
    }

Нагадування про оплату TurboSMS

    // ключ HTTP API
    $apiKey = 'xxxxxxxxxxxxxxxxxx';
    // поріг коштів після якого нотифікувати
    $lowerLimit = 4000; 
 
    $apiCallback = 'http://api.turbosms.ua/user/balance.json';
    $turboSmsApi = new OmaeUrl($apiCallback);
    $turboSmsApi->dataGet('token', $apiKey);
    $balanceRaw = $turboSmsApi->response();
 
    $watchdogCallbackResult = '';
    if (!empty($balanceRaw)) {
        @$balanceRaw = json_decode($balanceRaw, true);
        if (!empty($balanceRaw)) {
             if (isset($balanceRaw['response_result'])) {
                    if (isset($balanceRaw['response_result']['balance'])) {
                        $balance = $balanceRaw['response_result']['balance'];
                        if ($balance>=$lowerLimit) {
                            $watchdogCallbackResult.=' - коштів достатньо. ';
                        } else {
                            $watchdogCallbackResult.=' - добре би поповнити! ';
                        }
                    }
             }
        }
    }
 

Моніторинг живості OLT-ів

$watchdogCallbackResult = ' ';
$deadOltCount=0;
$deadSwitches = zb_SwitchesGetAllDead();
 
if (!empty($deadSwitches)) {
    $switchesDb=new NyanOrm('switches');
    $switchesDb->where('desc','LIKE','%OLT%');
    $allOlts=$switchesDb->getAll('ip');
    if (!empty($allOlts)) {
        foreach ($allOlts as $oltIp=>$eachOltData) {
            if (isset($deadSwitches[$oltIp])) {
                $deadOltCount++;
            }
        }   
    }
}
 
if ($deadOltCount>0) {
    $watchdogCallbackResult .= $deadOltCount.' завернулись шубою. Можна починати панікувати!';
} else {
    $watchdogCallbackResult .= 'зі всіма наразі все гаразд.';
}
watchdog.txt · Востаннє змінено: 2024/07/29 16:23 повз nightfly