<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Posts on YmnukTech</title>
        <link>https://ymnuktech.ru/posts/</link>
        <description>Recent content in Posts on YmnukTech</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>ru</language>
        <lastBuildDate>Sat, 05 Aug 2023 00:00:00 +0000</lastBuildDate>
        <atom:link href="https://ymnuktech.ru/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Умный чат на собственном компьютере.</title>
            <link>https://ymnuktech.ru/posts/2023/08/%D1%83%D0%BC%D0%BD%D1%8B%D0%B9-%D1%87%D0%B0%D1%82-%D0%BD%D0%B0-%D1%81%D0%BE%D0%B1%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%BC%D0%BF%D1%8C%D1%8E%D1%82%D0%B5%D1%80%D0%B5./</link>
            <pubDate>Sat, 05 Aug 2023 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2023/08/%D1%83%D0%BC%D0%BD%D1%8B%D0%B9-%D1%87%D0%B0%D1%82-%D0%BD%D0%B0-%D1%81%D0%BE%D0%B1%D1%81%D1%82%D0%B2%D0%B5%D0%BD%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%BC%D0%BF%D1%8C%D1%8E%D1%82%D0%B5%D1%80%D0%B5./</guid>
            <description>&lt;p&gt;Сколько шума было вокруг ChatGPT? Я лично его так и не пробовал. Не знаю грустно это или весело — не знаю. Но можно запустить прям дома, только вот параметры компа нужны тоже достаточно… нормальные. Лично я запускал на 6 ядрах и 16ГБ ОЗУ, но об этом далее.&lt;/p&gt;
&lt;h2 id=&#34;подготовка&#34;&gt;Подготовка&lt;/h2&gt;
&lt;p&gt;Для начала немного разберемся что у нас есть. А есть у нас нейронные сети и знания о том, что они очень медленно работают на CPU, а для ускорения нужно иметь GPU. Но хочется на CPU.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Сколько шума было вокруг ChatGPT? Я лично его так и не пробовал. Не знаю грустно это или весело — не знаю. Но можно запустить прям дома, только вот параметры компа нужны тоже достаточно… нормальные. Лично я запускал на 6 ядрах и 16ГБ ОЗУ, но об этом далее.</p>
<h2 id="подготовка">Подготовка</h2>
<p>Для начала немного разберемся что у нас есть. А есть у нас нейронные сети и знания о том, что они очень медленно работают на CPU, а для ускорения нужно иметь GPU. Но хочется на CPU.</p>
<p>Далее есть ОС «Форточка» и «Пингвин». Я буду делать в пингвинах.</p>
<p>Теперь нужны программы для работы. Я использовал <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> и <a href="https://github.com/LostRuins/koboldcpp">koboldcpp</a>. Первая нужна для подготовки моделей, а вторая для их непосредственного запуска. Можно и в первой запускать модели, но есть, как оказывается, нюансы.</p>
<p>Закидываем к себе в закладки <a href="https://huggingface.co">Hugging Face</a>, так как отсюда будем забирать модели.</p>
<p>И так, у меня на текущий момент установлен Debian 12, а это значит, что будем ставить пакеты в нем. Ставим недостающие пакеты:</p>
<p>sudo apt install build-essential libopenblas-dev git git-lfs python3-pip</p>
<p>Для питона, если еще не сделано, то создаем файл <strong>~/.config/pip/pip.conf</strong> и записываем в него следующее содержимое:</p>
<p>[global]
break-system-packages = true</p>
<p>Этот файл нужен, чтобы можно было установить пакеты питона, если не удается их установить, а они понадобятся (благо их немного).</p>
<h2 id="компиляция">Компиляция</h2>
<p>Качаем софт:</p>
<p>git clone <a href="https://github.com/ggerganov/llama.cpp">https://github.com/ggerganov/llama.cpp</a></p>
<p>git clone <a href="https://github.com/LostRuins/koboldcpp">https://github.com/LostRuins/koboldcpp</a></p>
<p>Переходим в директорию <strong>llama.cpp</strong> и ставим пакеты:</p>
<p>pip install -r requirements.txt</p>
<p>И собираем саму приложуху.</p>
<p>make</p>
<p>Теперь переходим в <strong>koboldcpp</strong> и ставим пакеты оттуда:</p>
<p>pip install -r requirements.txt</p>
<p>И собираем пакет:</p>
<p>LLAMA_OPENBLAS=1 make</p>
<p>Вот тут есть нюанс. Дело в том, что в Linux нужна дополнительная для работы этого движка (если честно я не сильно разобрался). Но… openBLAS нам подходит. Можно собрать с поддержкой cuBLAS, но нужно установить CUDA, а для этого видеокарта должна поддерживать технологию. Кроме того нужно много библиотек и свежие драйвера. Если интересно как ставить, то идем <a href="https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html">сюда</a>. Тут много умных слов, так что читаем и изучаем, если еще не в курсе. А это все нужно для установки компилятора <strong>nvcc</strong> и без него никак.</p>
<h2 id="модели">Модели</h2>
<p>Я покажу 2 способа использования моделей.</p>
<h3 id="сборка-модели-из-pytorch">Сборка модели из PyTorch</h3>
<p>Есть модели, которые построены в PyTorch, но их нужно конвертировать в формат ggml. Для начала скачаем модель. Для этого рядом с директориями <strong>llama.cpp</strong> и <strong>koboldcpp</strong>, создадим директорию <strong>models</strong> и перейдем в нее. Теперь скачаем модель:</p>
<p>git clone <a href="https://huggingface.co/IlyaGusev/llama_7b_ru_turbo_alpaca_lora_merged">https://huggingface.co/IlyaGusev/llama_7b_ru_turbo_alpaca_lora_merged</a></p>
<p>После скачивания перейдем обратно в llama.cpp и сделаем конвертирование:</p>
<p>python3 convert-pth-to-ggml.py ../models/llama_7b_ru_turbo_alpaca_lora_merged 1</p>
<p>Этой коммандой мы переводим формат PyTorch в GGML F16 (float16)</p>
<p>./quantize ../models/llama_7b_ru_turbo_alpaca_lora_merged/ggml-model.bin ../models/alpaca-7b-ru-q4_1.bin 3</p>
<p>Этой коммандой мы делаем «квантование», тем самым уменьшая объем модели. Да, в качестве мы тоже теряем, но нужна производительность.</p>
<h3 id="готовые-модели">Готовые модели</h3>
<p>Вернемся в директорию <strong>models</strong> и загрузим нужные модели:</p>
<p>git clone <a href="https://huggingface.co/IlyaGusev/llama_7b_ru_turbo_alpaca_lora_llamacpp">https://huggingface.co/IlyaGusev/llama_7b_ru_turbo_alpaca_lora_llamacpp</a></p>
<p>git clone <a href="https://huggingface.co/IlyaGusev/llama_13b_ru_turbo_alpaca_lora_llamacpp">https://huggingface.co/IlyaGusev/llama_13b_ru_turbo_alpaca_lora_llamacpp</a></p>
<p>Соответственно первая весит 4ГБ, вторая около 8ГБ. Чтобы определить какая больше понравится, нужно поиграться с обеими.</p>
<h2 id="запуск">Запуск</h2>
<p>Теперь вернемся в koboldcpp и запустим какую-нибудь модель (у нас их 3):</p>
<p>python3 koboldcpp.py —model ../models/llama_7b_ru_turbo_alpaca_lora_llamacpp/7B/ggml-model-q4_0.bin —host 0.0.0.0 —port 8085</p>
<p>Это пример запуска модели. Можно открыть в браузере адрес <strong>localhost:8085</strong> и пользоваться. Соответственно другие модели запускаются так же.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-1024x712.png" alt="Пример работы модели Alpaca 7B ru как умного чата"></p>
<h2 id="нюанс">Нюанс</h2>
<p>Если модель сконвертировать в <strong>llama.cpp</strong>, то можно запустить прямо в ней, но вот модели, загруженные уже готовыми то <strong>llama.cpp</strong> их не запустить (у меня падала с ошибкой) и вот тут приходит на помощь <strong>koboldcpp</strong>. К тому же мне показалось, что у него и возможностей побольше и поинтереснее (если ковырнуть UI и параметры которые он отправляет в запросе).</p>
<h2 id="ложка-дёгтя">Ложка дёгтя</h2>
<p>Во первых — это не оригинальный ChatGPT, во вторых модели 7B и 13B все же маленькие по сравнению с большими братьями, и в третьих llama — проект другой компании со своей лицензией.</p>
<h1 id="выводы">Выводы</h1>
<p>Проектов нейронных сетей в конкретно этом направлении достаточно много. В частности для этого кода основные, которые я лично нашел, 3 — llama, alpaca и vicuna. У Alpaca есть доученный русский корпус и он достаточно адекватен (на 7B и 13B).</p>
<p>Вообще такую модель можно запустить и на <a href="https://ymnuktech.ru/home-server-with-gui/">домашнем сервере</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Централизованное чтение логов в Windows</title>
            <link>https://ymnuktech.ru/posts/2023/05/%D1%86%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%B2-windows/</link>
            <pubDate>Thu, 04 May 2023 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2023/05/%D1%86%D0%B5%D0%BD%D1%82%D1%80%D0%B0%D0%BB%D0%B8%D0%B7%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D1%87%D1%82%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%B2-windows/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png&#34; alt=&#34;Журналы Windows&#34;&gt;&lt;/p&gt;
&lt;p&gt;Ранее уже было написано у меня &lt;a href=&#34;https://ymnuktech.ru/server-logs-and-collect&#34;&gt;о сборе логов&lt;/a&gt; с различных серверов и хранения их в единой БД. Сегодня будет заметка для сбора логов.&lt;/p&gt;
&lt;h2 id=&#34;софт-и-настройка&#34;&gt;Софт и настройка&lt;/h2&gt;
&lt;p&gt;Логи нужно собирать, в первую очередь, с серверов, так что это достаточно важная задача. А вот чем собирать — вопрос. Можно отправлять файлы на сервер и там их парсить и анализировать. Можно поставить специальный софт, который сам это будет делать и отправлять на сервер готовые записи (этот больше подходит, так как удобнее). Теперь сам софт.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png" alt="Журналы Windows"></p>
<p>Ранее уже было написано у меня <a href="https://ymnuktech.ru/server-logs-and-collect">о сборе логов</a> с различных серверов и хранения их в единой БД. Сегодня будет заметка для сбора логов.</p>
<h2 id="софт-и-настройка">Софт и настройка</h2>
<p>Логи нужно собирать, в первую очередь, с серверов, так что это достаточно важная задача. А вот чем собирать — вопрос. Можно отправлять файлы на сервер и там их парсить и анализировать. Можно поставить специальный софт, который сам это будет делать и отправлять на сервер готовые записи (этот больше подходит, так как удобнее). Теперь сам софт.</p>
<p>Я лично попробовал 2 программы: <a href="https://code.google.com/archive/p/eventlog-to-syslog/">eventlog-to-syslog</a> и <a href="https://nxlog.co/">nxlog</a>. Первый был обновлен в 2013 году и отправляет на сервер логи в формате syslog, но то что он «пихает» все что можно в виде строки, в дальнейшем тяжело парсить. В итоге получается ну очень длинное регулярное выражение, в котором можно легко запутаться. Вторая софтина есть в 3х вариантах: Enterprise Edition, Manager и Community Edition. Первый вариант для корпоративных клиентов (платная), вторая для сообщества (бесплатная). В Community Edition есть ряд ограничений, но мне этого хватает: получение событий, преобразование в JSON, отправка на удаленный сервер по TCP и UDP.</p>
<p>Регистрируемся на сайте и скачиваем файл для винды. В итоге устанавливаем msi-файл. Далее по пути *<em>C:\Program Files\nxlog\conf*</em> (если не меняли стандартный путь) и файл <strong>nxlog.conf</strong> приводим к следующему виду:</p>
<pre><code>Module      xm_syslog

Module      xm_charconv
AutodetectCharsets iso8859-2, utf-8, utf-16, utf-32

Module      xm_exec

Module      xm_fileop

# Check the size of our log file hourly, rotate if larger than 5MB

    Every   1 hour
    Exec    if (file_exists('%LOGFILE%') and \
               (file_size('%LOGFILE%') &gt;= 5M)) \
                file_cycle('%LOGFILE%', 8);

# Rotate our log file every week on Sunday at midnight

    When    @weekly
    Exec    if file_exists('%LOGFILE%') file_cycle('%LOGFILE%', 8);

Module    xm_json

Module    im_msvistalog
Exec      to_json();
</code></pre>
<h1 id="snare-compatible-example-configuration">Snare compatible example configuration</h1>
<h1 id="collecting-event-log">Collecting event log</h1>
<h1></h1>
<h1 id="module------im_msvistalog">Module      im_msvistalog</h1>
<h1></h1>
<h1></h1>
<h1 id="converting-events-to-snare-format-and-sending-them-out-over-tcp-syslog">Converting events to Snare format and sending them out over TCP syslog</h1>
<h1></h1>
<h1 id="module------om_tcp">Module      om_tcp</h1>
<h1 id="host--------19216811">Host        192.168.1.1</h1>
<h1 id="port--------514">Port        514</h1>
<h1 id="exec--------to_syslog_snare">Exec        to_syslog_snare();</h1>
<h1></h1>
<h1></h1>
<h1 id="connect-input-in-to-output-out">Connect input &lsquo;in&rsquo; to output &lsquo;out&rsquo;</h1>
<h1></h1>
<h1 id="path--------in--out">Path        in =&gt; out</h1>
<h1></h1>
<pre><code>Module    om_udp
Host      ip-адрес_сервера
Port      5517

Path        eventlog =&gt; udp
</code></pre>
<p>Я специально оставил стандартные тэги из оригинального файла, чтобы было проще и наглядней искать и ориентироваться в нем.</p>
<h2 id="на-сервере">На сервере</h2>
<p>Собирает у нас эти логи все тот же <strong>fluentd</strong> (winlog.conf):</p>
<p>@type udp
tag winlog</p>
<pre><code>@type json
</code></pre>
<p>port 5517
bind 0.0.0.0
message_length_limit 10MB</p>
<pre><code>@type record_modifier

    EventTimeMillis ${time.nsec}
</code></pre>
<p>@type http
open_timeout 2
endpoint http://clickhouse:port/?user=login&amp;password=password&amp;database=logs&amp;query=INSERT%20INTO%20winlog%20FORMAT%20JSONEachRow</p>
<pre><code>@type json
</code></pre>
<p>json_array true</p>
<pre><code>flush_interval 10s
</code></pre>
<p>На забываем добавить строчку <strong>@include conf.d/winlog.conf</strong> в основной конфиг.</p>
<h2 id="бд">БД</h2>
<p>Я думал ClickHouse меня пошлет на поляну одуванчики собирать, но пока что все обошлось:</p>
<p>CREATE TABLE IF NOT EXISTS logs.winlog (
EventTime DateTime,
EventTimeMillis UInt64,
Hostname String default &lsquo;unknow&rsquo;,
Keywords Nullable(Int64),
EventType Nullable(String),
SeverityValue Nullable(Int16),
Severity Nullable(String),
EventID Nullable(Int32),
SourceName Nullable(String),
ProviderGuid Nullable(String),
Version Nullable(Int32),
Task Nullable(Int32),
OpcodeValue Nullable(Int32),
RecordNumber Nullable(Int64),
ActivityID Nullable(String),
CallerPID Nullable(String),
ProcessID Nullable(Int32),
ThreadID Nullable(Int32),
Channel Nullable(String),
SChannelName Nullable(String),
SChannelType Nullable(String),
Domain Nullable(String),
AccountName Nullable(String),
UserID Nullable(String),
UserName Nullable(String),
DomainName Nullable(String),
WorkstationName Nullable(String),
SessionId Nullable(String),
LogonGuid Nullable(String),
VolumeGuid Nullable(String),
VolumeNameLength Nullable(String),
VolumeName Nullable(String),
VolumeLabelLength Nullable(String),
VolumeLabel Nullable(String),
Error Nullable(String),
TransmittedServices Nullable(String),
AccountType Nullable(String),
Message Nullable(String),
Category Nullable(String),
TicketOptions Nullable(String),
Opcode Nullable(String),
CSEElaspedTimeInMilliSeconds Nullable(String),
ErrorCode Nullable(String),
CSEExtensionName Nullable(String),
CSEExtensionId Nullable(String),
operationName Nullable(String),
OperationDescription Nullable(String),
OperationElaspedTimeInMilliSeconds Nullable(String),
PolicyActivityId Nullable(String),
PolicyApplicationMode Nullable(String),
PolicyDownloadTimeElapsedInMilliseconds Nullable(String),
PrincipalSamName Nullable(String),
IsMachine Nullable(String),
IsDomainJoined Nullable(String),
IsBackgroundProcessing Nullable(String),
IsAsyncProcessing Nullable(String),
IsServiceRestart Nullable(String),
ReasonForSyncProcessing Nullable(String),
Profiles Nullable(String),
SettingType Nullable(String),
SettingValueSize Nullable(String),
SettingValue Nullable(String),
SettingValueString Nullable(String),
Origin Nullable(String),
ModifyingUser Nullable(String),
ModifyingApplication Nullable(String),
Environment Nullable(String),
EventsUploaded Nullable(String),
EventsDropped Nullable(String),
LastEventlogWrittenTime Nullable(String),
PackageName Nullable(String),
TargetUserName Nullable(String),
TargetSid Nullable(String),
TargetDomainName Nullable(String),
ServiceName Nullable(String),
ServiceSid Nullable(String),
serviceStatus Nullable(String),
Workstation Nullable(String),
Status Nullable(String),
TicketEncryptionType Nullable(String),
PreAuthType Nullable(String),
IpAddress Nullable(String),
IpPort Nullable(String),
PortNumber Nullable(String),
PathID Nullable(String),
TargetID Nullable(String),
LUN Nullable(String),
ClassDeviceGuid Nullable(String),
AdapterGuid Nullable(String),
BusType Nullable(String),
MiniportName Nullable(String),
VendorId Nullable(String),
ProductId Nullable(String),
RequestDuration_ms Nullable(String),
WaitDuration_ms Nullable(String),
Command Nullable(String),
SrbStatus Nullable(String),
ScsiStatus Nullable(String),
SenseKey Nullable(String),
AddSense Nullable(String),
AddSenseQ Nullable(String),
IoSize Nullable(String),
QueueDepth Nullable(String),
LBA Nullable(String),
TaskInstanceId Nullable(String),
EnginePID Nullable(String),
LogString Nullable(String),
SubjectUserSid  Nullable(String),
SubjectUserName Nullable(String),
SubjectDomainName Nullable(String),
SubjectLogonId Nullable(String),
ObjectServer Nullable(String),
ObjectType Nullable(String),
ObjectName Nullable(String),
HandleId Nullable(String),
TransactionId Nullable(String),
AccessList Nullable(String),
AccessMask Nullable(String),
PrivilegeList Nullable(String),
ProcessName Nullable(String),
ResourceAttributes Nullable(String),
param1 Nullable(String),
param2 Nullable(String),
param3 Nullable(String),
param4 Nullable(String),
param5 Nullable(String),
ClientLUID Nullable(String),
ClientUserName Nullable(String),
resourceUri Nullable(String),
SMBShare Nullable(String),
Vcb Nullable(String),
ClientDomainName Nullable(String),
TaskName Nullable(String),
MechanismOID Nullable(String),
OldSd Nullable(String),
Reason Nullable(String),
InstanceId Nullable(String),
IsExtensionAsyncProcessing Nullable(String),
exception Nullable(String),
VolumeCorrelationId Nullable(String),
DfsNamespace Nullable(String),
NewSd Nullable(String),
InfoDescription Nullable(String),
SyncFromPDC Nullable(String),
Type Nullable(String),
Path Nullable(String),
Priority Nullable(String),
RefreshTriggerSource Nullable(String),
SyncType Nullable(String),
SamAccountName Nullable(String),
Parameter Nullable(String),
hrError Nullable(String),
SidHistory Nullable(String),
DCName Nullable(String),
DCIPAddress Nullable(String),
TimeConsumedInMilliSeconds Nullable(String),
Machines Nullable(String),
Dummy Nullable(String),
DisplayName Nullable(String),
DCDiscoveryTimeInMilliSeconds Nullable(String),
UserNameLength Nullable(String),
UserPrincipalName Nullable(String),
MachineRole Nullable(String),
ID Nullable(String),
UserContext Nullable(String),
DeviceGUID Nullable(String),
CallerProcessId Nullable(String),
CallerProcessName Nullable(String),
PrincipalCNName Nullable(String),
PrincipalDomainName Nullable(String),
DCDomainName Nullable(String),
serverName Nullable(String),
namespaceName Nullable(String),
wmiClassName Nullable(String),
methodName Nullable(String),
protocol Nullable(String),
Name Nullable(String),
DeviceNameLength Nullable(String),
LowestFreeSpaceInBytes Nullable(String),
IsBootVolume Nullable(String),
ActionName Nullable(String),
FolderId Nullable(String),
HighIoLatencyCount Nullable(String),
IntervalDurationUs Nullable(String),
NCReadIOCount Nullable(String),
NCReadTotalBytes Nullable(String),
NCReadAvgLatencyNs Nullable(String),
NCWriteIOCount Nullable(String),
NCWriteTotalBytes Nullable(String),
NCWriteAvgLatencyNs Nullable(String),
FileFlushCount Nullable(String),
FileFlushAvgLatencyNs Nullable(String),
VolumeFlushCount Nullable(String),
VolumeFlushAvgLatencyNs Nullable(String),
FileLevelTrimCount Nullable(String),
FileLevelTrimTotalBytes Nullable(String),
FileLevelTrimExtentsCount Nullable(String),
FileLevelTrimAvgLatencyNs Nullable(String),
VolumeTrimCount Nullable(String),
VolumeTrimTotalBytes Nullable(String),
VolumeTrimExtentsCount Nullable(String),
VolumeTrimAvgLatencyNs Nullable(String),
IoBucketsCount Nullable(String),
TotalBytesBucketsCount Nullable(String),
ExtentsBucketsCount Nullable(String),
IoCount Nullable(String),
TotalLatencyUs Nullable(String),
TotalBytes Nullable(String),
TrimExtentsCount Nullable(String),
IoTypeIndex Nullable(String),
ServerNameLength Nullable(String),
ServerName Nullable(String),
AllNtpServers Nullable(String),
TickCount Nullable(String),
IFTSTMP Nullable(String),
GPOList Nullable(String),
XPath Nullable(String),
LastError Nullable(String),
PolicyProcessingMode Nullable(String),
ResultCode Nullable(String),
BandwidthInkbps Nullable(String),
ComputerAccountChange Nullable(String),
HomeDirectory Nullable(String),
HomePath Nullable(String),
ScriptPath Nullable(String),
ProfilePath Nullable(String),
UserWorkstations Nullable(String),
PasswordLastSet Nullable(String),
AccountExpires Nullable(String),
PrimaryGroupId Nullable(String),
AllowedToDelegateTo Nullable(String),
OldUacValue Nullable(String),
NewUacValue Nullable(String),
UserAccountControl Nullable(String),
UserParameters Nullable(String),
LogonHours Nullable(String),
DnsHostName Nullable(String),
ServicePrincipalNames Nullable(String),
IsSlowLink Nullable(String),
ThresholdInkbps Nullable(String),
LinkDescription Nullable(String),
NumberOfGPOsDownloaded Nullable(String),
NumberOfGPOsApplicable Nullable(String),
GPODownloadTimeElapsedInMilliseconds Nullable(String),
DescriptionString Nullable(String),
GPOInfoList Nullable(String),
IsGPOListChanged Nullable(String),
GPOListStatusString Nullable(String),
ApplicableGPOList Nullable(String),
DeviceName Nullable(String),
PolicyElaspedTimeInSeconds Nullable(String),
IsConnectivityFailure Nullable(String),
NextPolicyApplicationTime Nullable(String),
NextPolicyApplicationTimeUnit Nullable(String),
EventReceivedTime Nullable(DateTime),
SourceModuleName Nullable(String),
SourceModuleType Nullable(String)
)
ENGINE = ReplacingMergeTree() PARTITION BY toDate(EventTime)
ORDER BY (EventTime, EventTimeMillis, Hostname)
TTL EventTime + toIntervalMonth(6)
SETTINGS index_granularity = 8192;</p>
<p>Да, такая таблица получилась. Дальше только анализировать в различных инструментах.</p>
<h2 id="заключение">Заключение</h2>
<p>Собственно это все. После сбора определенного количества логов можно приступать к анализу деятельности узла, которое и хотим отслеживать. Судя по структуре так же можно в дальнейшем дела корреляцию между узлами, например, чтение файлов на файл-сервере и клиенте, который запрашивает этот файл и т.д.</p>
]]></content>
        </item>
        
        <item>
            <title>Анализ логов</title>
            <link>https://ymnuktech.ru/posts/2022/12/%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</link>
            <pubDate>Mon, 12 Dec 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/12/%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</guid>
            <description>&lt;p&gt;Ура! У нас есть накапливаемая БД в &lt;a href=&#34;https://ymnuktech.ru/storage-logs-in-db&#34;&gt;логами&lt;/a&gt;! Дальше только анализ логов и разбор полетов…&lt;/p&gt;
&lt;h2 id=&#34;графики-логов&#34;&gt;Графики логов&lt;/h2&gt;
&lt;p&gt;Ну вот не знаю что с этим делать и как дальше жить. Из готового особо ничего такого не обнаружил (может все же плохо искал). Из того что есть — это графики логов, т.е. пишем специальный запрос и смотрим как это «красиво» рисуется.&lt;/p&gt;
&lt;h2 id=&#34;чем-же-смотреть&#34;&gt;Чем же смотреть&lt;/h2&gt;
&lt;p&gt;Из того что я нашел более простое в освоении и «красивое» — это &lt;a href=&#34;https://grafana.com/grafana&#34;&gt;Grafana&lt;/a&gt;.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Ура! У нас есть накапливаемая БД в <a href="https://ymnuktech.ru/storage-logs-in-db">логами</a>! Дальше только анализ логов и разбор полетов…</p>
<h2 id="графики-логов">Графики логов</h2>
<p>Ну вот не знаю что с этим делать и как дальше жить. Из готового особо ничего такого не обнаружил (может все же плохо искал). Из того что есть — это графики логов, т.е. пишем специальный запрос и смотрим как это «красиво» рисуется.</p>
<h2 id="чем-же-смотреть">Чем же смотреть</h2>
<p>Из того что я нашел более простое в освоении и «красивое» — это <a href="https://grafana.com/grafana">Grafana</a>.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-1-1024x345.png" alt="Пример графика">Пример графика</p>
<p>Я думаю многие видели это решение, по этому не очень хочется заострять на нем внимание. Единственный момент — это работа с ClickHouse.</p>
<h3 id="подключение-clickhouse">Подключение ClickHouse</h3>
<p>Тут все предельно просто:</p>
<ul>
<li>Ставим плагин</li>
</ul>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-2-1024x289.png" alt="Плагин в Grafana">Плагин в Grafana</p>
<ol start="2">
<li>Настраиваем подключение к БД</li>
</ol>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-3.png" alt="DataSource">DataSource</p>
<ol start="3">
<li>Пишем запрос для отображения</li>
</ol>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-4-1024x493.png" alt="Запрос для прокси-сервера">Запрос для прокси-сервера</p>
<p>В принципе тут все. Все стандартненько.</p>
<h2 id="свое-решение">Свое решение</h2>
<p>Другой вариант — это собственное решение. Нам важнее было анализировать работу через proxy-сервер пользователей за какой-то период. Например нам нужно было узнать кто потратил трафик за период, на каких доменах тусовался, может даже ссылочки посмотреть какие видюшки рассматривал и т.д. Может я что-то в Grafana и не осилил, но собственное решение появилось раньше.</p>
<p>Так как это все тот же ClickHouse, и в нем выполняется самый обычный SQL, то для меня (а уже и для коллег, которые с SQL-запросами не работали, но научились) это проще. Ну лень мне учить очередной язык чего-то там супер модного.</p>
<p>Для начала можно написать запрос в каком-нибудь готовом UI, чтобы его отладить. Далее можно было бы сделать какую-то оболочку, в которую «загоняется» написанный запрос и при его вызове появлялась бы простенькая форма для ввода нужных значений и получения результата.</p>
<p>Само решение приводить не буду, так как оно ну оооочень и слиииишком сырое, но работает уже около полутора лет. Приведу только некоторые моменты…</p>
<p>Сама система написана в связке NodeJS (backend) + Angular и используется 2 коннектора для СУБД: <a href="https://www.npmjs.com/package/clickhouse">SQLite</a> (ORM <a href="https://www.npmjs.com/package/sequelize">Sequelize</a>) и<a href="https://www.npmjs.com/package/clickhouse">ClickHouse</a>. Собственно в SQLite хранятся разделы и непосредственно запросы. Тут тоже ничего сверхъестественного нет (2 таблицы). <em>group.js</em>:</p>
<pre tabindex="0"><code>
&#39;use strict&#39;;
const {
  Model
} = require(&#39;sequelize&#39;);
module.exports = (sequelize, DataTypes) =&gt; {
  class Group extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
      Group.hasMany(models.Query, {
        foreignKey: &#39;id_group&#39;
      });
    }
  };
  Group.init({
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true,
      allowNull: false
    },
    name: {
      type: DataTypes.STRING,
      allowNull: false
    },
    description: DataTypes.TEXT
  }, {
    sequelize,
    modelName: &#39;Group&#39;,
  });
  return Group;
};
</code></pre><p>и query.js:</p>
<pre tabindex="0"><code>
&#39;use strict&#39;;
const {
  Model
} = require(&#39;sequelize&#39;);
module.exports = (sequelize, DataTypes) =&gt; {
  class Query extends Model {
    /**
     * Helper method for defining associations.
     * This method is not a part of Sequelize lifecycle.
     * The `models/index` file will call this method automatically.
     */
    static associate(models) {
      // define association here
      Query.belongsTo(models.Group, {
        foreignKey: &#39;id_group&#39;
      });
    }
  };
  Query.init({
    id: {
      type: DataTypes.UUID,
      primaryKey: true,
      allowNull: false,
      defaultValue: DataTypes.UUIDV4
    },
    name: {
      type: DataTypes.STRING,
      allowNulld: false
    },
    description: DataTypes.TEXT,
    querytext: {
      type: DataTypes.TEXT,
      allowNull: false
    }
  }, {
    sequelize,
    modelName: &#39;Query&#39;,
  });
  return Query;
};
</code></pre><p>Немного <a href="https://www.npmjs.com/package/express">express.js</a>по вкусу и в принципе всё (даже приводить не буду — все стандартно).</p>
<p>Собственно нужно выполнить запрос с параметрами. Для этого нужно как-то запрос параметризировать, да еще и вменяемые параметры для отображения использовать.</p>
<p>Решение получилось такое: берется самый обычный запрос и в месте, где должен быть параметр водставляется строка вида <strong>{{Название параметра}}</strong>. Такой параметр всегда будет принимать строку, а в коде будет самая обычная конкатенация строк (для простоты):</p>
<pre tabindex="0"><code>
const re = /\{\{[\s\t]*[a-zа-я_]+[a-zа-я_0-9]+[\s\t]*\}\}/ig;

/**
 * Найдем все параметры в строке запроса
 * @param {string} querytext Текст запроса
 * @return Возвращает массив с параметрами
 */
function parsingQueryParams(querytext) {
    let m;
    let arr = [];
    do {
        m = re.exec(querytext);
        if (m) {
            console.log(m[0]);
            let tmp = /[a-zа-я_]+[a-zа-я_0-9]/i.exec(m[0])[0];
            if (arr.length === 0) {
                arr.push(tmp);
            } else {
                // Проверим не присутствует ли уже этот параметр
                let search = false;
                for (const str of arr) {
                    if (str === tmp) {
                        search = true;
                        break;
                    }
                }
                if (!search) {
                    arr.push(tmp);
                }
            }
            // arr.push(/[a-zа-я_]+[a-zа-я_0-9]/i.exec(m[0])[0]);
        }
    } while (m);
    return arr;
}

/**
 * Подготовка запроса для выполнения
 * @param {string} querytext Текст запроса
 * @param {Object} params Параметры ключ=знаяение
 * @return Возвращает строку с замененными параметрами
 */
function replaceQueryParams(querytext, params) {
    let m;
    do {
        m = re.exec(querytext);
        if (m) {
            let reTmp = m[0];
            let param = reTmp.replace(/[\{\{\}\}\s\t]+/gi, &#39;&#39;);
            if (params.hasOwnProperty(param)) {
                querytext = querytext.replace(reTmp, params[param]);
            }
        }
    } while (m);
    return querytext;
}

module.exports = {
    parsingQueryParams: parsingQueryParams,
    replaceQueryParams: replaceQueryParams
}
</code></pre><p>Клиентский код тоже достаточно прост:</p>
<pre tabindex="0"><code>
import { Component, Inject, Input, OnInit } from &#39;@angular/core&#39;;
import { FormControl, FormGroup } from &#39;@angular/forms&#39;;
import { MatDialogRef, MAT_DIALOG_DATA } from &#39;@angular/material/dialog&#39;;
import * as parsingQuery from &#39;../../../../../server/libs/parsingQuery&#39;;
export interface DisplayField {
  name: string;
  display?: string;
}
@Component({
  selector: &#39;app-query-exec&#39;,
  templateUrl: &#39;./query-exec.component.html&#39;,
  styleUrls: [&#39;./query-exec.component.styl&#39;]
})
export class QueryExecComponent implements OnInit {
  fields = new Array(0);
  form: FormGroup = new FormGroup({});
  constructor(
    public dialogRef: MatDialogRef,
    @Inject(MAT_DIALOG_DATA) public data: string
  ) { }
  ngOnInit(): void {
    const params = parsingQuery.parsingQueryParams(this.data);
    this.fields = new Array();
    for (const str of params) {
      let display: string = str.replace(/_{1,1000}/gi, &#39; &#39;).trim().toLowerCase();
      display = display.trim().substr(0, 1).toUpperCase() + display.substr(1);
      this.fields.push({
        name: str,
        display
      });
    }
    for (const control of Object.keys(this.form.controls)) {
      this.form.removeControl(control);
    }
    for (const field of this.fields) {
      this.form.addControl(field.name, new FormControl());
    }
  }
  private prepareParams(): any {
    const result: any = {};
    result.params = {};
    for (const field of this.fields) {
      result.params[field.name] = this.form.value[field.name];
      /*result.params.push({
        name: field.name,
        value: this.form.value[field.name]
      });*/
    }
    return result;
  }
  queryTable(): void {
    const result: any = this.prepareParams();
    result.typeQuery = &#39;dataset&#39;;
    this.dialogRef.close(result);
  }
  queryGraph(): void {
    const result: any = this.prepareParams();
    result.typeQuery = &#39;graph&#39;;
    this.dialogRef.close(result);
    // TODO
  }
}
</code></pre><p>Ну и форма к ней:</p>
<pre tabindex="0"><code>
    Параметры запроса

                    {{ field.display }}

            Таблица
            График
</code></pre><p>Собственно всё!</p>
<h2 id="результаты">Результаты</h2>
<p>В итоге свою задачу мы решили и пользуемся. Нам хватает с головой, при учете 6 серверов и около 200 пользователей.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-5-1024x663.png" alt="ТОП-100 доменов по пользователю за день">ТОП-100 доменов по пользователю за день</p>
]]></content>
        </item>
        
        <item>
            <title>Хранение логов в БД</title>
            <link>https://ymnuktech.ru/posts/2022/12/%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%B2-%D0%B1%D0%B4/</link>
            <pubDate>Thu, 08 Dec 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/12/%D1%85%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%B2-%D0%B1%D0%B4/</guid>
            <description>&lt;p&gt;После анализа &lt;a href=&#34;https://ymnuktech.ru/collect-logs&#34;&gt;логов&lt;/a&gt; их необходимо куда-то структурированно сохранить (какую-то базу). Для хранения логов в БД для начала нужно выбрать в какую СУБД. При этом существует, опять же, целая масса вариантов.&lt;/p&gt;
&lt;p&gt;Немного поразмыслив я решил использовать ClickHouse. Судя по обзорам достаточно быстрая (не зря же yandex ее создавали как раз для хранения большого объема и обработки аналитики). Можно было бы выбрать тот же PostgreSQL, MySQL (MariaDB), Elasticsearch… В общем все что угодно, но я остановился на этом варианте.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>После анализа <a href="https://ymnuktech.ru/collect-logs">логов</a> их необходимо куда-то структурированно сохранить (какую-то базу). Для хранения логов в БД для начала нужно выбрать в какую СУБД. При этом существует, опять же, целая масса вариантов.</p>
<p>Немного поразмыслив я решил использовать ClickHouse. Судя по обзорам достаточно быстрая (не зря же yandex ее создавали как раз для хранения большого объема и обработки аналитики). Можно было бы выбрать тот же PostgreSQL, MySQL (MariaDB), Elasticsearch… В общем все что угодно, но я остановился на этом варианте.</p>
<h2 id="установка">Установка</h2>
<p>Для установки обращаемся к <a href="https://clickhouse.com/docs/ru/getting-started/install/">официальной документации</a> и выполняем следующие команды:</p>
<pre tabindex="0"><code>
sudo apt-get install -y apt-transport-https ca-certificates dirmngr
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 8919F6BD2B48D754

echo &#34;deb https://packages.clickhouse.com/deb stable main&#34; | sudo tee /etc/apt/sources.list.d/clickhouse.list
sudo apt-get update
sudo apt-get install -y clickhouse-server clickhouse-client
sudo service clickhouse-server start
clickhouse-client
</code></pre><p>Так как я это все разворачиваю на Debian, то и способ установки выбираю соответствующий. Вообще ничего сложного.</p>
<p>После установки рекомендую настроить сервер: установить пароль и настроить использование ресурсов (если нужно). Все это есть в <a href="https://clickhouse.com/docs/ru/">документации</a>.</p>
<p>Так же можно посмотреть в сторону какого-нибудь UI для удобства работы. Что-то конкретно рекомендовать не буду, так как в поисковике должно много чего найтись.</p>
<h2 id="структура">Структура</h2>
<p>Для начала нужно создать саму базу:</p>
<pre tabindex="0"><code>
CREATE DATABASE IF NOT EXISTS logs
</code></pre><p>Теперь создадим таблицы в БД. Для логов Nginx:</p>
<pre tabindex="0"><code>
CREATE TABLE IF NOT EXISTS logs.nginx (
time DateTime,
dt_syslog String,
server String,
programm String,
remote String,
host String,
user String,
method String,
path String,
proto String,
code UInt16,
size UInt32,
referer String,
agent String,
http_x_forwarded_for String
)
ENGINE = ReplacingMergeTree() PARTITION BY (toDate(time), server, programm)
ORDER BY (time, server, programm, size)
TTL time + toIntervalMonth(6)
SETTINGS index_granularity = 8192;
</code></pre><p>Далее таблица для Postfix:</p>
<pre tabindex="0"><code>
CREATE TABLE IF NOT EXISTS logs.postfix (
time DateTime,
server String,
daemon String,
process String,
process_id String,
connect_to_host String,
connect_to_ip String,
port UInt16,
status String,
connect_from String,
connect_ip_from String,
disconnect_from String,
disconnect_ip_from String,
ehlo Int16,
starttls Int16,
mail Int16,
rcpt Int16,
data Int32,
quit Int16,
commands Int16,
lost_conn_host String,
lost_conn_ip String,
queue_id String,
removed String,
client String,
client_ip String,
message_id String,
to String,
orig_to String,
relay_host String,
relay_ip String,
delay Float64,
delays String,
dsn String,
description String,
from String,
size UInt32,
nrcpt UInt16
)
ENGINE = ReplacingMergeTree() PARTITION BY (toDate(time), server, daemon, process)
ORDER BY (time, server, daemon, process, queue_id, message_id)
TTL time + toIntervalMonth(6)
SETTINGS index_granularity = 8192;
</code></pre><p>И в заключении таблица для Squid:</p>
<pre tabindex="0"><code>
CREATE TABLE IF NOT EXISTS logs.squid (
dt DateTime,
millis UInt16,
server String,
processTime UInt32,
ipClient String,
statusName String,
statusCode UInt16,
size UInt32,
method String,
url String,
domain String,
authName String,
proxyResult String,
proxyAddress String,
contentType String
) ENGINE = ReplacingMergeTree() PARTITION BY toDate(dt)
ORDER BY (dt, millis, server)
TTL time + toIntervalMonth(6)
SETTINGS index_granularity = 8192;
</code></pre><p>Для всех таблиц используется параметр TTL, который учитывается при оптимизации таблиц. В данном случае в таблицу «можно» вставлять дублирующие строки, но чтобы от них избавиться в запросе после названия таблицы, нужно вставить <a href="https://clickhouse.com/docs/ru/sql-reference/statements/select/from/#select-from-final">FINAL</a>, ну и, соответственно, при оптимизации так же дублирующие строки должны быть удалены. Признак дублирования строк основывается на параметре ORDER BY для «движка» <a href="https://clickhouse.com/docs/ru/engines/table-engines/mergetree-family/replacingmergetree/">ReplacingMergeTree</a>.</p>
<h3 id="обслуживание">Обслуживание</h3>
<p>Для обслуживания (уменьшения дублирующих строк) я использую следующий скрипт:</p>
<pre tabindex="0"><code>
#!/bin/bash

LOGIN=login
PASSWORD=password
HOST=address
echo &#34;OPTIMIZE TABLE logs.squid FINAL DEDUPLICATE&#34; | clickhouse-client -u $LOGIN --password $PASSWORD --host $HOST
echo &#34;OPTIMIZE TABLE logs.nginx FINAL DEDUPLICATE&#34; | clickhouse-client -u $LOGIN --password $PASSWORD --host $HOST
echo &#34;OPTIMIZE TABLE logs.postfix FINAL DEDUPLICATE&#34; | clickhouse-client -u $LOGIN --password $PASSWORD --host $HOST
</code></pre><p>Ну и закинуть «сие чудо» в планировщик.</p>
<h2 id="заключение">Заключение</h2>
<p>Далее можно переходить к поиску информации уже непосредственно в БД.</p>
]]></content>
        </item>
        
        <item>
            <title>Обработка логов</title>
            <link>https://ymnuktech.ru/posts/2022/12/%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</link>
            <pubDate>Mon, 05 Dec 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/12/%D0%BE%D0%B1%D1%80%D0%B0%D0%B1%D0%BE%D1%82%D0%BA%D0%B0-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</guid>
            <description>&lt;p&gt;Логи, собственно, уже &lt;a href=&#34;https://ymnuktech.ru/send-logs-syslog&#34;&gt;собираем&lt;/a&gt;. Следующая задача их нужно каким-то образом обработать логи: подготовить и отправить в базу данных.&lt;/p&gt;
&lt;h2 id=&#34;чем-обрабатывать&#34;&gt;Чем обрабатывать?&lt;/h2&gt;
&lt;p&gt;В принципе вариантов на эту тему масса. Первый вариант — использовать готовое решение, второй вариант — писать свое. Собственно сложность состоит в том, что хоть и логи +/- похожи друг на друга, но все же имеют отличия. Возьмем первый вариант и попробуем его реализовать.&lt;/p&gt;
&lt;p&gt;Для получения и парсинга логов, дума, можно взять fluentd. Продукт открытый, документация объемная, готовые плагины тоже есть. Попробуем разобраться.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Логи, собственно, уже <a href="https://ymnuktech.ru/send-logs-syslog">собираем</a>. Следующая задача их нужно каким-то образом обработать логи: подготовить и отправить в базу данных.</p>
<h2 id="чем-обрабатывать">Чем обрабатывать?</h2>
<p>В принципе вариантов на эту тему масса. Первый вариант — использовать готовое решение, второй вариант — писать свое. Собственно сложность состоит в том, что хоть и логи +/- похожи друг на друга, но все же имеют отличия. Возьмем первый вариант и попробуем его реализовать.</p>
<p>Для получения и парсинга логов, дума, можно взять fluentd. Продукт открытый, документация объемная, готовые плагины тоже есть. Попробуем разобраться.</p>
<h2 id="парсим-логи">Парсим логи</h2>
<p>Для начала его нужно установить. Идем на <a href="https://docs.fluentd.org/installation/install-by-deb">официальную страницу</a> и смотрим установку:</p>
<pre tabindex="0"><code>
curl -fsSL https://toolbelt.treasuredata.com/sh/install-debian-bullseye-td-agent4.sh | sh
</code></pre><p>Вроде как все просто.</p>
<p>Следующий момент — взять файл с логами и обработать его. Для этого будем использовать уже готовую директорию с логами, которую получаем с других серверов.</p>
<p>Но перед парсингом нужно установить еще один плагин:</p>
<pre tabindex="0"><code>
td-agent-gem install fluent-plugin-record-modifier --no-document
</code></pre><p>Этот плагин понадобится для удобной модификации записей. Теперь у нас практически все готово для работы. Приступим…</p>
<h3 id="nginx">NGinx</h3>
<p>Создаем файл /etc/td-agent/conf.d/nginx_site.conf:</p>
<pre tabindex="0"><code>
  @type tail
  tag nginx.site
  path /var/log/10.x.x.x/nginx_snipchi.log
  pos_file /var/log/td-agent/nginx_snipchi.log.pos
  @log_level info
  @id nginx_site

    @type regexp
    expression ^(?[^ ]*\s+[^ ]*\s+[^ ]*)\s+(?[^ ]*) (?[^ ]*) (?[^ ]*) (?[^ ]*) (?[^ ]*) \[(?[^\]]*)\] &#34;(?\S+)(?: +(?[^ ]*?) (?[^&#34;]*)?)?&#34; (?[^ ]*) (?[^ ]*)(?: &#34;(?[^\&#34;]*)&#34; &#34;(?[^\&#34;]*)&#34;(?:\s+(?[^ ]+))?)?$
    time_format %d/%b/%Y:%H:%M:%S %z

    @type record_modifier

        time ${time}

#
#    @type stdout
#

  @type http
  #open_timeout 2

  endpoint http://10.x.x.x:8123/?user=login&amp;password=password&amp;database=logs&amp;query=INSERT%20INTO%20nginx%20FORMAT%20JSONEachRow

    @type json

  json_array true

    flush_interval 10s
</code></pre><p>В данном случае мы берем файл с диска и обрабатываем его построчно выделяя поля из записи с помощью регулярных выражений. Далее прогоняем через фильтр и уже в конце через директиву «match» собираем обработанные записи, накапливаем и отправляем уже в БД в формате JSON. Здесь я использую ClickHouse, но об это я напишу в следующей заметке. Здесь стоит запомнить лишь то, что <strong>fluent</strong> берет итоговый фвйл JSON и отправляет POST-запросом по протоколу http на указанный URI, а вот ClickHouse в свою очередь спокойно получает запрос и полученный JSON просто раскладывает по таблице.</p>
<p>Если присмотреться в раздел <strong>source</strong>, то в нашем случае важны следующие параметры:</p>
<p>@type — тип источника</p>
<p>tag — тэг для записей лога</p>
<p>path — путь к файлу лога</p>
<p>pos_file — что-то типа pipe-файла. Он нужен для fluent</p>
<p>@log_level — какие события отфильтровывать</p>
<p>Раздел <strong>parse</strong> в <strong>source</strong> объясняет источнику как распарсить полученную запись лога. Для этого используются следующие директивы:</p>
<p>@type — тип будущей записи</p>
<p>expression — регулярное выражение для парсинга записи и дальнейшего преобразования в <strong>JSON</strong></p>
<p>time_format — формат времени чтобы система смогла определить формат и преобразовать строку даты/времени в числовое значение.</p>
<p>Еще стоит обрабтить внимание, что один из блоков <strong>match</strong> закомментирован. Его я использовал просто для отладки. В нем полученный результат записывается в лог самого fluent.</p>
<h3 id="squid">Squid</h3>
<p>Создаем файл /etc/td-agent/conf.d/squid.conf:</p>
<pre tabindex="0"><code>
  @type tail
  tag squid.access
  path /var/log/10.x.x.x/squid-access.log
  pos_file /var/log/td-agent/squid-access.log.pos
  @log_level info
  @id squid

    @type regexp
    expression ^(?[^ ]*\s+[^ ]*\s+[^ ]*)\s+(?[^ ]*)\s+(?[^ ]*)\s+(?[^\.]*)\.(?[^ ]*)\s+(?[^ ]*)\s+(?[^ ]*)\s+(?[^/]*)/(?[^ ]*)\s+(?[^ ]*)\s+(?[^ ]*)\s+(?[^ ]*)\s+(?[^ ]*)\s+(?[^/]*)/(?[^ ]*)\s+(?[^\s$]*)
    time_format %b %d %H:%M:%S %z

    @type record_modifier
    remove_keys dt_syslog, programm

        domain ${record[&#39;url&#39;]}

        key domain
        #expression (http(s)?://)?(?[^\s\t/:$]*).*
        expression (?http(s)?:\/\/)
        replace &#34;&#34;

        key domain
        expression (?:[^\/$]*)
        replace &#34;&#34;

        key domain
        expression \/.*$
        replace &#34;&#34;

#
#    @type stdout
#
#       @type json
#
#

  @type http
  open_timeout 2

  endpoint http://10.x.x.x:8123/?user=login&amp;password=password&amp;database=logs&amp;query=INSERT%20INTO%20squid%20FORMAT%20JSONEachRow

    @type json

  json_array true

    flush_interval 10s
</code></pre><p>Эта настройка предназначена для парсинга логов прокси-сервера Squid. По сути в нем так же ничего сверхъестественного нет. Вся разница заключается в разделе <strong>source</strong>, а конкретней в <strong>expression</strong>.</p>
<h3 id="электронная-почта">Электронная почта</h3>
<p>В моем случае используется Postfix. Вот у него логи немного сложнее, но не намного. Стоит проявить немного внимания и уситчивости и все получится.</p>
<p>Создаем файл /etc/td-agent/conf.d/postfix.conf:</p>
<pre tabindex="0"><code>
  type tail
  path /var/log/10.x.x.x/postfix.log
  tag postfix.relay
  #format /^(?[^ ]+) (?[^ ]+) (?[^:]+): (?((?[^ :]+)[ :])? ?((to|from)=[^&gt;]+)&gt;)?.*)$/
  #format /(?[\w]+\s+[\d]+\s[\d:]+)\s+(?.+)/
  time_format %b %d %H:%M:%S
  #format none
  pos_file /var/log/td-agent/postfix-relay.log.pos

        @type regexp
        expression ^(?[^ ]*\s+[^ ]*\s+[^ ]*)\s+(?[^ ]*)\s+(?[^/]*)/(?[^\[]*)\[(?[^\]]*)\]:\s+(connect to (?[^\[]+)\[(?[^\]]+)\](:(?[^:]+):\s(?.+)?)?)?(connect\sfrom\s(?[^\[]+)\[(?[^\]]+)\])?(disconnect\sfrom\s(?[^\[]+)\[(?[^\]]+)\]\sehlo=(?[^ ]+)\sstarttls=(?[^ ]+)\smail=(?[^ ]+)\srcpt=(?[^ ]+)\sdata=(?[^ ]+)\squit=(?[^ ]+)\scommands=(?.+))?(lost connection after CONNECT from (?[^\[]+)\[(?[^\]]+))?((?[^:]+): (removed(?))?(client=(?[^\[]+)\[(?[^\]]+\]))?(message\-id=[^&gt;]+))?)?(to=[^&gt;]+)&gt;, (orig_to=[^&gt;]+)&gt;, )?(relay=(?[^\[]+)\[(?[^\]]+)\](:(?\d+))?)?(,\sdelay=(?[^,]+))?(,\sdelays=(?[^,]+))?(,\sdsn=(?[^,]*))?(,\sstatus=(?[^ ]*)\s)?(?.+)?)?(from=[^&gt;]+)&gt;,\ssize=(?[^,]+),\snrcpt=(?[^ $]+)(\s(?.+))?)?
        time_format %b %d %H:%M:%S

    @type record_modifier

        time ${time}

#
#    @type stdout
#
#       @type json
#
#

  @type http
  #open_timeout 2

  endpoint http://10.x.x.x:8123/?user=login&amp;password=password&amp;database=logs&amp;query=INSERT%20INTO%20postfix%20FORMAT%20JSONEachRow

    @type json

  json_array true

    flush_interval 10s
</code></pre><p>Собственно вся загвоздка в том, что по сути <strong>Postfix</strong> имеет многострочные логи, а если быть точнее, то логи однострочные, но форматы строк все разные (ну или почти). Но это тоже решается.</p>
<h2 id="немного-велосипеда">Немного велосипеда</h2>
<p>Если смотреть в документации <a href="http://docs.fluentd.org">fluent</a>, то можно обнаружить, что формат парсинга для того же Nginx уже есть, но он не будет работать, так как после приема логов через <strong>syslog</strong> в результирующий файл попадает еще несколько дополнительных полей в начало записи. Так что пришлось немного переписать. А вот как это поправить в rsyslog — я, честно, не разобрался.</p>
<p>Кроме всего прочего, судя по документации <strong><a href="https://docs.fluentd.org/input/syslog">fluent</a></strong> умеет самостоятельно принимать логи в формате syslog и можно было бы отказаться от серверного rsyslog, но мы так прикинули, что пусть уж лучше будет что-то стандартное, тем более событий не так много, а временно хранящиеся файлы на диске в течении какого-то времени, как бы, хороший тон.</p>
<h2 id="работай">Работай!</h2>
<p>После написания правил обработки данных их неплохо было бы включить, иначе работать просто не будут. Для этого редктируем файл /etc/td-agent/td-agent.conf:</p>
<pre tabindex="0"><code>
@include conf.d/nginx_site.conf
@include conf.d/postfix.conf
@include conf.d/squid.conf
</code></pre><p>После можно перезапустить агента:</p>
<pre tabindex="0"><code>
systemctl restart td-agent
</code></pre><h2 id="замечание">Замечание</h2>
<p>При написании правил порядок разделов имеет значение. Если сначала написать **** а потом ****, то работать не будет. соответственно и обработчики, например ****, тоже должны идти последовательно в том порядке, который того требует обработка данных.</p>
<h2 id="результат">Результат</h2>
<p>Сейчас есть обработчик на одном сервере, который собирает события откуда требуется, обрабатывает и отправляет их в базу. Теперь нужно решить вопрос о хранении.</p>
]]></content>
        </item>
        
        <item>
            <title>Передача логов на один сервер</title>
            <link>https://ymnuktech.ru/posts/2022/12/%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%87%D0%B0-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%BD%D0%B0-%D0%BE%D0%B4%D0%B8%D0%BD-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</link>
            <pubDate>Thu, 01 Dec 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/12/%D0%BF%D0%B5%D1%80%D0%B5%D0%B4%D0%B0%D1%87%D0%B0-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2-%D0%BD%D0%B0-%D0%BE%D0%B4%D0%B8%D0%BD-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</guid>
            <description>&lt;p&gt;Вот не хочется устанавливать ворох ПО на серверы &lt;a href=&#34;https://ymnuktech.ru/server-logs-and-collect&#34;&gt;для сбора логов&lt;/a&gt; в одном месте. Хочется использовать что-то уже  предустановленное.  Будем организовывать передачу логов на один сервер (может на 2, а может и на 3 — все зависит от нагрузок на систему и передаваемого трафика). В данной статье речь пойдет об операционных системах Debian и CentOS (просто они у нас есть).&lt;/p&gt;
&lt;h2 id=&#34;серверная-часть&#34;&gt;Серверная часть&lt;/h2&gt;
&lt;p&gt;В общем в подавляющем большинстве на этих ОС rsyslog уже установлен, по этому почему бы не взять его? Ну значит так и сделаем!&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Вот не хочется устанавливать ворох ПО на серверы <a href="https://ymnuktech.ru/server-logs-and-collect">для сбора логов</a> в одном месте. Хочется использовать что-то уже  предустановленное.  Будем организовывать передачу логов на один сервер (может на 2, а может и на 3 — все зависит от нагрузок на систему и передаваемого трафика). В данной статье речь пойдет об операционных системах Debian и CentOS (просто они у нас есть).</p>
<h2 id="серверная-часть">Серверная часть</h2>
<p>В общем в подавляющем большинстве на этих ОС rsyslog уже установлен, по этому почему бы не взять его? Ну значит так и сделаем!</p>
<p>Первоначально выбираем узел (машину) (в моем случае Debian 11), где мы будем все это собирать, и добавляем все в конфигурационный файл по пути <em>/etc/rsyslog.d/server.conf</em> и записываем следующее содержимое в файл:</p>
<pre tabindex="0"><code>
# provides UDP syslog reception
module(load=&#34;imudp&#34;)
input(type=&#34;imudp&#34; port=&#34;5514&#34;)

# provides TCP syslog reception
module(load=&#34;imtcp&#34;)
input(type=&#34;imtcp&#34; port=&#34;5514&#34;)

$template remote-incoming-logs,&#34;/var/log/%FROMHOST-IP%/%PROGRAMNAME%.log&#34;
*.* ?remote-incoming-logs
</code></pre><p>В данном файле я разрешаю создание сервера по протоколу <strong>tcp</strong> и <strong>udp</strong>. Порт указан отличный от стандартного. Стандартный же порт <strong>514</strong>. Так же в моем случае при входящих событиях я указываю шаблом пути сохранения логов в директорию ip-адреса и программы, логи которой необходимо получить.</p>
<p>После перезапускаем службу:</p>
<pre tabindex="0"><code>
systemctl restart rsyslog
</code></pre><h2 id="клиентская-часть">Клиентская часть</h2>
<p>С другой стороны все тоже достаточно просто. На другом сервере по пути <em>/etc/rsyslog.d/client.conf</em> вписываем следующие настройки:</p>
<pre tabindex="0"><code>
#Enable sending system logs over UDP to rsyslog server
*.* @:5514
#Enable sending system logs over TCP to rsyslog server
*.* @@ip-адрес сервера для отправки:5514
$ActionQueueFileName queue
$ActionQueueMaxDiskSpace 10g
$ActionQueueSaveOnShutdown on
$ActionQueueType LinkedList
$ActionResumeRetryCount -1
</code></pre><p>Тут тоже все достаточно просто, но с парой нюансов.</p>
<p>Есть 2 правила, которые говорят отправлять данные на удаленный сервер, которые практически одинаковые, но все же разница есть. Первое правило с одним символом «собаки» (@) указывает клиенту, что отправка производится по протоколу UDP, в с двумя «собаками» — по TCP.</p>
<p>Следующие 5 строчек обозначают, что данные нужно кэшировать (если удаленный сервер будет недоступен): имя файла очереди, размер очереди, сохранять очередь при выключении, использовать <a href="https://www.rsyslog.com/doc/master/concepts/queues.html">связный список</a>, в случае неудачной попытки отправки данных пытаться отправлять данные «бесконечно».</p>
<p>В принципе это тот минимум, который требуется (для начала).</p>
<p>После перезапуска службы на сервере сбора логов можно увидеть создаваемые директории с логами, которые отправляют клиенты.</p>
<h2 id="nginx">Nginx</h2>
<p>В сервер Nginx есть встроенный модуль для отправки логов через rsyslog. Более подробно об этом можно почитать на <a href="http://nginx.org/ru/docs/syslog.html">официальном сайте</a>.</p>
<p>Чтобы передавать логи в конфигурационном файлы в секции server необходимо добавить несколько строк:</p>
<pre tabindex="0"><code>
access_log syslog:server=:,facility=local7,tag=,severity=info combined;
error_log syslog:server=: debug;
</code></pre><p>Если на сервере несколько сайтов, то данные настройки нужно прописать на каждый из них.</p>
<p>После настройки перезапускаем сервер:</p>
<pre tabindex="0"><code>
nginx -t
nginx -s reload
</code></pre><h2 id="прощай-место">Прощай место</h2>
<p>Так как логи пишутся постоянно, то они имеют тенденцию «съедать» диск. Как-то нужно их перерабатывать. Для таких целей в системе еще одна предустановленная служба — <a href="https://github.com/logrotate/logrotate">logrotate</a>.</p>
<p>Возвращаемся на сервер, где у нас собираются логи и открываем файл /etc/logrotate.d/remote:</p>
<pre tabindex="0"><code>
var/log/127.0.0.1/*.log
/var/log/10.x.x.x/*.log
/var/log/10.x.x.x/*.log
/var/log/10.x.x.x/*.log {
    hourly
    missingok
    rotate 1
    compress
    notifempty
    sharedscripts
    postrotate /usr/lib/rsyslog/rsyslog-rotate
    endscript
}
</code></pre><p>Заместо <em>10.x.x.x</em> подставьте свои директории.</p>
<p>В настройках указано, перерабатывать каждый час, хранить один файл истории, сжимать… Более подробно лучше всего обратиться к <a href="https://www.google.com/search?q=logrotate">документации</a> и <a href="https://yandex.ru/search/?text=logrotate">документации2</a> <em>(man logrotate)</em>.</p>
<h2 id="что-в-итоге">Что в итоге?</h2>
<p>В итоге должен получится сервер по сбору логов с других серверов через интерфейс syslog.</p>
]]></content>
        </item>
        
        <item>
            <title>Сервер логирования и сбор логов</title>
            <link>https://ymnuktech.ru/posts/2022/10/%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%BB%D0%BE%D0%B3%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F-%D0%B8-%D1%81%D0%B1%D0%BE%D1%80-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</link>
            <pubDate>Fri, 28 Oct 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/10/%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%BB%D0%BE%D0%B3%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F-%D0%B8-%D1%81%D0%B1%D0%BE%D1%80-%D0%BB%D0%BE%D0%B3%D0%BE%D0%B2/</guid>
            <description>&lt;p&gt;Логирование событий — очень нужная вещь. Лучше же конечно не откладывать данную проблему в долгий ящик (если, конечно, у вас не один сервер, а целый парк). В такой конфигурации не набегаешься по серверам и текстовиков не насмотришься. А если еще их и анализировать нужно, то мои поздравления — мы попали в бездну. И вот тут бело бы неплохо настроить сервер логирования и сбор логов в одном месте, чтобы можно было получить доступ к данным.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Логирование событий — очень нужная вещь. Лучше же конечно не откладывать данную проблему в долгий ящик (если, конечно, у вас не один сервер, а целый парк). В такой конфигурации не набегаешься по серверам и текстовиков не насмотришься. А если еще их и анализировать нужно, то мои поздравления — мы попали в бездну. И вот тут бело бы неплохо настроить сервер логирования и сбор логов в одном месте, чтобы можно было получить доступ к данным.</p>
<p>Ну что же, будем пробовать воплотить все это в жизнь.</p>
<h2 id="а-что-собирать-то">А что собирать то?</h2>
<p>Что обычно собирают? Наверное же все… В моем случае «хочется» собирать события с web-серверов, баз данных, планировщиков событий, почтовых серверов, события аутентификаций пользователей с серверов, прокси-серверов и события передачи пакетов на серверах (да, и такое в том числе). Вот на счет последнего лично еще не понимаю как это делать, но, думаю, по ходу дела уже будем определяться что логировать и сколько времени все это будет храниться.</p>
<h2 id="требования-к-инфраструктуре">Требования к инфраструктуре</h2>
<p>Попробуем определиться с тем что у нас есть. В моем случае это парк серверов под управлением Linux (причем вполне себе разных дистрибутивов) и несколько Windows-серверов.</p>
<p>Все это хочется собирать в единой БД (или кластере БД, в зависит от того что хочется получить) и желательно чтобы можно было все это анализировать простыми запросами. Я лично привык работать с SQL (удобно, блин) — значит нужно что-то в этом направлении. Мир СУБД с SQL очень обширный: PostgreSQL, MySQL/MariaDB, ClickHouse и т.д. В общем выбор достаточно обширен. Можно (а может и нужно), конечно, использовать NoSQL, типа как MongoDB, Elasticsearch, Hadoop (что там еще есть), но видимо я их не осилил (хотя с MongoDB работал — небольшой опыт есть).</p>
<p>Далее неплохо было бы определиться с инструментарием сбора логов и его передачи. Тут в принципе все тоже достаточно интересно: rsyslog, syslog-ng, logstash, fluentd и  что-то еще (будем разбираться).</p>
<p>На выходе должен получиться какой-то интерфейс для отображения всего этого добра в виде анализа списков, графиков, оповещений (это пока что только фантазии).</p>
<h2 id="конец-но-не-конец">Конец, но не конец.</h2>
<p>Это только определение с тем, что мне нужно получить в итоге. В дальнейшем сервер логирования и сбора логов будем делать поэтапно.</p>
]]></content>
        </item>
        
        <item>
            <title>Генерация тайлов своими мозгами</title>
            <link>https://ymnuktech.ru/posts/2022/06/%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D0%BC%D0%BE%D0%B7%D0%B3%D0%B0%D0%BC%D0%B8/</link>
            <pubDate>Thu, 16 Jun 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/06/%D0%B3%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D1%8F-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D0%BC%D0%BE%D0%B7%D0%B3%D0%B0%D0%BC%D0%B8/</guid>
            <description>&lt;p&gt;Я уже писал про &lt;a href=&#34;https://ymnuktech.ru/openstreetmap-tile-server&#34;&gt;тайловый сервер&lt;/a&gt; на собственных вычислительных мощностях, но вот что-то захотелось сделать еще что-то интересное и более настраиваемое. Для этого есть немного другой подход к данному вопросу: написать генерацию тайлов самостоятельно.&lt;/p&gt;
&lt;p&gt;В данном случае получится не совсем полностью собственные алгоритмы, но тоже достаточно занимательно.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/map.png&#34; alt=&#34;Тайл, сгенерированный на NodeJS &amp;#43; mapnik&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;с-чего-начать&#34;&gt;С чего начать?&lt;/h2&gt;
&lt;p&gt;Первым делом требуется определиться что есть уже готовое. Одним из компонентов является &lt;a href=&#34;https://mapnik.org/&#34;&gt;mapnik&lt;/a&gt;. Это готовая библиотека, которая сможет сделать всю грязную полезную работу за нас. Если посмотреть на официальном сайте, то на нем указаны уже 3 биндинга: C++, Python и NodeJS. Я взял третий вариант, так как с ним все достаточно просто и мне знаком. На Python тоже можно сделать, но я лично использую именно Node. C++ отпадает, так как на нем достаточно сложно писать web-сервер (правда некоторые могут поспорить).&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про <a href="https://ymnuktech.ru/openstreetmap-tile-server">тайловый сервер</a> на собственных вычислительных мощностях, но вот что-то захотелось сделать еще что-то интересное и более настраиваемое. Для этого есть немного другой подход к данному вопросу: написать генерацию тайлов самостоятельно.</p>
<p>В данном случае получится не совсем полностью собственные алгоритмы, но тоже достаточно занимательно.</p>
<p><img src="/images/posts/map.png" alt="Тайл, сгенерированный на NodeJS &#43; mapnik"></p>
<h2 id="с-чего-начать">С чего начать?</h2>
<p>Первым делом требуется определиться что есть уже готовое. Одним из компонентов является <a href="https://mapnik.org/">mapnik</a>. Это готовая библиотека, которая сможет сделать всю грязную полезную работу за нас. Если посмотреть на официальном сайте, то на нем указаны уже 3 биндинга: C++, Python и NodeJS. Я взял третий вариант, так как с ним все достаточно просто и мне знаком. На Python тоже можно сделать, но я лично использую именно Node. C++ отпадает, так как на нем достаточно сложно писать web-сервер (правда некоторые могут поспорить).</p>
<p>Следующим этапом нужно определиться с <a href="http://mapnik.org/documentation/node-mapnik/">документацией</a>. На самом деле с ней тоже все в порядке.</p>
<h2 id="каркас">Каркас</h2>
<p>Для работы со всем этим делом понадобится всего 4 модулю. Устанавливаем:</p>
<pre tabindex="0"><code>
npm i express generic-pool mapnik mkdirp
</code></pre><p>Далее создаем файл <strong>libs/mapnik.js</strong>:</p>
<pre tabindex="0"><code>
var mapnik = require(&#39;mapnik&#39;);

mapnik.register_fonts(&#39;/usr/share/fonts&#39;, { recurse: true });
mapnik.register_default_input_plugins();

module.exports = () =&gt; {
    let map = new mapnik.Map(256, 256);
    map.loadSync(&#39;./openstreetmap-carto/mapnik.xml&#39;);
    map.registerFonts(&#39;/usr/share/fonts&#39;, { recurse: true });
    map.loadFonts();
    map.zoomAll();
    return map;
}
</code></pre><p>После создаем пул в файле <strong>libs/pool.js</strong>:</p>
<pre tabindex="0"><code>
const genericPool = require(&#39;generic-pool&#39;);
const mapnik = require(&#39;./mapnik&#39;);

const factory = {
    create: () =&gt; {
        return mapnik();
    },
    destroy: (client) =&gt; {
        console.log(`client destroyed`);
    }
};

const opts = {
    max: 10, // maximum size of the pool
    min: 1 // minimum size of the pool
};

const pool = genericPool.createPool(factory, opts);

module.exports = pool;
</code></pre><p>И, собственно, файл сервера <strong>index.js</strong>:</p>
<pre tabindex="0"><code>
const mapnik = require(&#39;mapnik&#39;);
const mkdirp = require(&#39;mkdirp&#39;);
const fs = require(&#39;fs&#39;);

const pool = require(&#39;./libs/pool&#39;);
const path = require(&#39;path&#39;)

const express = require(&#39;express&#39;)
const app = express()
const port = 3000

app.get(&#39;/:z/:x/:y.png&#39;, (req, res, next) =&gt; {
    // TODO
});

app.use((err, req, res, next) =&gt; {
    if (err) {
        return res.status(500).send(err.message);
    }
    return next();
});

app.listen(port, () =&gt; {
    console.log(`App listening on port ${port}`)
});
</code></pre><p><strong>libs/pool.js</strong> описывает пул экземпляров для <strong>mapnik</strong>, так как при конкурентном доступе к одному и тому же экземпляру будет возникать ошибка. Когда идет обращение к пулу, то он смотрит есть ли свободные экземпляры, и если их нет, то либо создает новые (если пул не заполнен), либо ждет, пока не будет возвращен занятый экземпляр.</p>
<h2 id="дополнительные-требования">Дополнительные требования</h2>
<p>Так же понадобится настройка стилей генерации тайлов. Тут 2 варианта: писать xml-файл вручную либо использовать готовый образец. Я, опять же, выбрал второй вариант. Для этого идем на сервер нашего тайлового сервера, который мы настраивали ранее и копируем к себе в рабочую директорию всю папку <strong>openstreetmap-carto</strong> со всеми сгенерированными настройками. В этой директории уже все есть, включая настройки подключения к БД и все необходимые файлы стилей.</p>
<h2 id="генерация-тайлов">Генерация тайлов</h2>
<p>Вот теперь можно заняться непосредственно кодом. В файле <strong>index.js</strong> в самом хэндлере <strong>get</strong> вместо <strong>TODO</strong> пишем такой код:</p>
<pre tabindex="0"><code>
if (fs.existsSync(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`)) {
        res.setHeader(&#39;content-type&#39;, &#39;image/png&#39;);
        return res.sendFile(path.resolve(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`));
    }

    pool.acquire().then(map =&gt; {

        mkdirp.sync(`./data/${req.params.z}/${req.params.x}`);
        let vt;
        try {
            vt = new mapnik.VectorTile(+req.params.z, +req.params.x, +req.params.y);
        } catch (e) {
            pool.release(map);
            return next(e);
        }

        map.render(vt, (err, vt) =&gt; {
            if (err) {
                pool.release(map);
                return next(err);
            }
            let image = new mapnik.Image(256, 256);
            vt.render(map, image, (err, image) =&gt; {
                if (err) {
                    pool.release(map);
                    return next(err);
                }
                image.encode(&#39;png8&#39;, (err, buffer) =&gt; {
                    if (err) {
                        pool.release(map);
                        return next(err);
                    }
                    fs.writeFile(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`, buffer, (err) =&gt; {
                        if (err) return next(err);
                        res.setHeader(&#39;content-type&#39;, &#39;image/png&#39;);
                        pool.release(map);
                        return res.sendFile(path.resolve(`./data/${req.params.z}/${req.params.x}/${req.params.y}.png`));
                    });
                });
            });

        });
    });
</code></pre><p>Здесь все достаточно просто: проверяем есть ли запрошенный тайл на диске, если нет, получаем экземпляр мапника и генерируем тайл с сохранением его на диск и отправкой клиенту, иначе спазу отдаем тайл с диска. Вот и весь код.</p>
<h2 id="выполняем">Выполняем</h2>
<p>Для выполнения озадачиваем командную строку такой конструкцией:</p>
<pre tabindex="0"><code>
node index.js
</code></pre><p>После открываем браузер и вбиваем адрес <strong>http://localhost:3000/0/0/0.png</strong>. Должен открыться тайл. Если нет, то проверяем ошибки.</p>
<h2 id="что-можно-еще-оптимизировать">Что можно еще оптимизировать?</h2>
<p>Теперь можно запускать сразу несколько экземпляров, например, через <a href="https://nodejs.org/api/cluster.html">NodeJS Cluster</a>, или упаковать в <a href="https://www.docker.com/">Docker</a> и запустить массу контейнеров, например, через <a href="https://yandex.ru/search/?text=docker&#43;%D0%BE%D1%80%D0%BA%D0%B5%D1%81%D1%82%D1%80%D0%B0%D1%82%D0%BE%D1%80&amp;lr=36">оркестратор</a>.</p>
<p>Ко всему прочему можно дописать кэширование тайлов в ОЗУ через <a href="https://redis.io/">Redis</a>, <a href="https://memcached.org/">memcache</a> и т.д. В таком варианте кластер все так же должен работать.</p>
<h2 id="итого">Итого</h2>
<p>Вот в таком варианте можно развивать сервис так, как того требует задача.</p>
]]></content>
        </item>
        
        <item>
            <title>OpenStreetMap — тайловый сервер — ускоряемся</title>
            <link>https://ymnuktech.ru/posts/2022/06/openstreetmap-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%83%D1%81%D0%BA%D0%BE%D1%80%D1%8F%D0%B5%D0%BC%D1%81%D1%8F/</link>
            <pubDate>Mon, 13 Jun 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/06/openstreetmap-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%83%D1%81%D0%BA%D0%BE%D1%80%D1%8F%D0%B5%D0%BC%D1%81%D1%8F/</guid>
            <description>&lt;p&gt;Про &lt;a href=&#34;https://ymnuktech.ru/openstreetmap-tile-server&#34;&gt;тайловый сервер&lt;/a&gt; я писал в другой статье, по этому повторяться не буду. Тут другая проблема возникла — скорость работы прям удручает…&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/map.png&#34; alt=&#34;Карта мира&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;в-чем-причина&#34;&gt;В чем причина?&lt;/h2&gt;
&lt;p&gt;Если долго разбираться, то в итоге проблема обнаруживается в СУБД. Скорость выполнения запроса 10 секунд — многовато. Когда тайлы начинают складываться, то карта жутко медленно загружается. Значит нужно что-то с ними сделать.&lt;/p&gt;
&lt;h2 id=&#34;что-получилось&#34;&gt;Что получилось?&lt;/h2&gt;
&lt;p&gt;Немного &lt;a href=&#34;https://yandex.ru/search/?text=Postgresql&amp;#43;%D0%BE%D1%82%D1%81%D0%BB%D0%B5%D0%B6%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5&amp;#43;%D0%BC%D0%B5%D0%B4%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D1%85&amp;#43;%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2&amp;amp;lr=36&#34;&gt;поотлавливав запросы в логах&lt;/a&gt;, я начал находить запросы, которые выполнялись по 3-4 секуды, а некоторые вообще 10-25 секунд. Это слишком много, даже если мы закэшируем все возможные варианты, ждать придется долго. В итоге у меня получился такой набор индексов:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Про <a href="https://ymnuktech.ru/openstreetmap-tile-server">тайловый сервер</a> я писал в другой статье, по этому повторяться не буду. Тут другая проблема возникла — скорость работы прям удручает…</p>
<p><img src="/images/posts/map.png" alt="Карта мира"></p>
<h2 id="в-чем-причина">В чем причина?</h2>
<p>Если долго разбираться, то в итоге проблема обнаруживается в СУБД. Скорость выполнения запроса 10 секунд — многовато. Когда тайлы начинают складываться, то карта жутко медленно загружается. Значит нужно что-то с ними сделать.</p>
<h2 id="что-получилось">Что получилось?</h2>
<p>Немного <a href="https://yandex.ru/search/?text=Postgresql&#43;%D0%BE%D1%82%D1%81%D0%BB%D0%B5%D0%B6%D0%B8%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5&#43;%D0%BC%D0%B5%D0%B4%D0%BB%D0%B5%D0%BD%D0%BD%D1%8B%D1%85&#43;%D0%B7%D0%B0%D0%BF%D1%80%D0%BE%D1%81%D0%BE%D0%B2&amp;lr=36">поотлавливав запросы в логах</a>, я начал находить запросы, которые выполнялись по 3-4 секуды, а некоторые вообще 10-25 секунд. Это слишком много, даже если мы закэшируем все возможные варианты, ждать придется долго. В итоге у меня получился такой набор индексов:</p>
<pre tabindex="0"><code>
CREATE INDEX idx_planet_osm_polygon_1 ON planet_osm_polygon (way_area DESC, name, boundary, admin_level, osm_id);
CREATE INDEX idx_planet_osm_polygon_2 ON planet_osm_polygon (way_area DESC, building, landuse, &#34;natural&#34;);
CREATE INDEX idx_planet_osm_polygon_3 ON planet_osm_polygon USING gist(way) WHERE
(
(landuse = ANY (ARRAY[&#39;forest&#39;::text, &#39;farmland&#39;::text, &#39;residential&#39;::text, &#39;commercial&#39;::text, &#39;retail&#39;::text, &#39;industrial&#39;::text, &#39;meadow&#39;::text, &#39;grass&#39;::text, &#39;village_green&#39;::text, &#39;vineyard&#39;::text, &#39;orchard&#39;::text]))
OR (&#34;natural&#34; = ANY (ARRAY[&#39;wood&#39;::text, &#39;wetland&#39;::text, &#39;mud&#39;::text, &#39;sand&#39;::text, &#39;scree&#39;::text, &#39;shingle&#39;::text, &#39;bare_rock&#39;::text, &#39;heath&#39;::text, &#39;grassland&#39;::text, &#39;scrub&#39;::text]))
)
AND building IS NULL;
CREATE INDEX idx_planet_osm_polygon_4 ON planet_osm_polygon (way_area DESC);

CREATE INDEX idx_planet_osm_point_osm_id ON planet_osm_point(osm_id);
CREATE INDEX idx_planet_osm_polygon_osm_id ON planet_osm_polygon(osm_id);
CREATE INDEX idx_planet_osm_line_osm_id ON planet_osm_line(osm_id);
</code></pre><p>Одни запросы сократились до 700мс, некоторые другие примерно 3,5 секунды. Это уже интереснее и гораздо быстрее, чем было ранее.</p>
<h2 id="нужно-больше-индексов">Нужно больше индексов</h2>
<p>В директории <strong>openstreetmap-carto</strong> я наткнулся еще на один файла: <strong>indexes.sql</strong>. Соответственно создал индексы из него, но не проверял на сколько там стало лучше.</p>
<h2 id="что-в-итоге">Что в итоге?</h2>
<p>В итоге генерация тайлов прям ожила на глазах. Сотрудник прям оценил по сравнению с предыдущим этапом. С такой картой уже можно работать!</p>
]]></content>
        </item>
        
        <item>
            <title>OpenStreetMap — тайловый сервер</title>
            <link>https://ymnuktech.ru/posts/2022/06/openstreetmap-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</link>
            <pubDate>Thu, 09 Jun 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/06/openstreetmap-%D1%82%D0%B0%D0%B9%D0%BB%D0%BE%D0%B2%D1%8B%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</guid>
            <description>&lt;p&gt;Есть такой ресурс под название &lt;a href=&#34;http://openstreetmap.org&#34;&gt;OpenStreetMap&lt;/a&gt;. Смысл его в том, что его может наполнять любой пользователь интернета. А еще его могут использовать для разных задач, например для поиска какого-то адреса, для путешествий, использование в качестве «подложки» для картографов и т.д. и т.п.&lt;/p&gt;
&lt;p&gt;И вот мне понадобилось его добавить в свой рабочий проект. Для этого есть 2 пути: использовать официальный сервис либо поднять свой сервер. Второй вариант может быть использован по ряду своих причин и какие были причины у меня останутся при мне.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Есть такой ресурс под название <a href="http://openstreetmap.org">OpenStreetMap</a>. Смысл его в том, что его может наполнять любой пользователь интернета. А еще его могут использовать для разных задач, например для поиска какого-то адреса, для путешествий, использование в качестве «подложки» для картографов и т.д. и т.п.</p>
<p>И вот мне понадобилось его добавить в свой рабочий проект. Для этого есть 2 пути: использовать официальный сервис либо поднять свой сервер. Второй вариант может быть использован по ряду своих причин и какие были причины у меня останутся при мне.</p>
<p><img src="/images/posts/map.png" alt="Пример сгенерированного тайла"></p>
<h2 id="подготовка">Подготовка</h2>
<p>Для начала нужно определиться на чем это все разворачивать. В моем распоряжении есть сервер с мелкомягкими на 192ГБ ОЗУ и 8 ядрами процессора (16 потоков). Теперь подумаем как это все расположить.</p>
<p>Так как это сервер уже с какими-то ГИСами, то плодить еще кучу виртуалок не хочется. Попробуем все это уместить в более скромный набор «всего» и «вся».</p>
<p>На сервер уже есть PostgreSQL 14, хоть и не настроенный, а просто установленный (он нужен и для других ГИС-систем).</p>
<p>Судя по просмотру материала тайловый сервер все равно придется делать на Linux. Тогда понадобится виртуальная машина. Тут снова 2 вариант: создать виртуалку на сервере виртуализации (у нас в основной массе используется ProxMox) либо создать виртуалку на Hyper-V. Второй вариант тоже предпочтительней, так как весь абор будет находиться на одной железке. Создаем виртуалку и выделяем ей 8ГБ ОЗУ и 4 ядра ЦПУ. В качестве ОС накатываем Линух (у меня Debian 11). После установки не забываем настроить адреса и SSH.</p>
<h2 id="вальс-в-троем">Вальс в троем</h2>
<p>Первым делом нужно установить <a href="http://postgis.net">PostGIS</a> (без него вообще никак) на Windows-сервер. Это специально расширение для PostgreSQL. Загружаем, устанавливаем. После открываем pgAdmin4 (он должен входить в стандартную поставку с PostgreSQL for Windows) и создаем пользователя для БД (в моем случае «osm», задаем пароль и выставляем для него права «Can login». Далее создаем БД для работы (в моем случае опять же «osm») и в качестве владельца указываем ранее созданного пользователя. После для БД выполняем запросы со страницы <a href="https://postgis.net/install/">https://postgis.net/install/</a>, чтобы расширение PostGIS установилось для базы и еще одну команду:</p>
<pre tabindex="0"><code>
CREATE EXTENSION hstore;
</code></pre><p>Расширение <strong>hstore</strong> нужно для хранения тэгов карты.</p>
<p>Сюда же нужно загрузить утилиту <a href="http://osm2pgsql.org">osm2pgsql</a>.</p>
<p>Теперь переходим на виртуалку иустанавливаем целый ворох программ:</p>
<pre tabindex="0"><code>
apt install mapnik lipapache2-mod-tile python-psycopg2 python2-yaml python3-yaml git npm nodejs fonts-noto-hinted fonts-noto-unhinted ttf-unifont fonts-noto-cjk fonts-hanazono libmapnik-dev mapnik-utils python3-mapnik
</code></pre><p>Далее понадобится пакет для генерации стиля тайлов карты:</p>
<pre tabindex="0"><code>
npm -g i carto
</code></pre><p>После загружаем непосредственно стили:</p>
<pre tabindex="0"><code>
git clone https://github.com/gravitystorm/openstreetmap-carto.git
cd openstreetmap-carto
</code></pre><p>Теперь открываем на редактирование файл <strong>project.mm</strong>, изменяем параметры подключения к БД в разделе osm2pgsql. В частности такие параметры как «<strong>dbname</strong>«,» <strong>host»</strong>, «<strong>user»</strong> и «<strong>password»</strong>. После выполняем команду:</p>
<pre tabindex="0"><code>
carto project.mm &gt; mapnik.xml
</code></pre><p>После подготовки необходимо скопировать файлы <strong>openstreet-carto.lua</strong> и <strong>openstreet-carto.style</strong> на Windows-сервер и положить их рядом с <strong>osm2pgsql</strong>.</p>
<p>Далее идем на сайт <a href="http://download.geofabrik.de">download.geofabrik.de</a>, выбираем нужную карту и загружаем «.osm.pbf».</p>
<p>Пока выполняется загрузка карты можно сделать настройку PostgreSQL (если до этого еще не было настроено). Для этого берем <em>PGTune</em> (ищем в интернете), указываем необходимые параметры, вбиваем в утилиту и получаем параметры настрокий. Загоняем их в postgresql.conf. Эту операцию нужно делать только в случае ПЕРВОНАЧАЛЬНОЙ УСТАНОВКИ PostgreSQL. Вообще, по хорошему, есть специальные рекомендации настрокий. Все это прекрасно гуглится и яндексится.</p>
<p>После загрузки карты и не долго думая импортируем карту в БД:</p>
<pre tabindex="0"><code>
osm2pgsql.exe -c -m -G --slim --hstore --tag-transform-script c:\maps\openstreet-carto.lua -d  -H localhost -U  -W -S c:\maps\openstreet-carto.style c:\maps\russia-latest.osm.pbf
</code></pre><p>Карта может импортироваться достаточно долго (в зависимости какая была выбрана). Возвращаемся на виртуалку и продолжаем настройку.</p>
<p>Здесь открываем <strong>/etc/renderd.conf</strong> и приводим примерно к следующему виду:</p>
<pre tabindex="0"><code>
[renderd]
stats_file=/run/renderd/renderd.stats
socketname=/run/renderd/renderd.sock
num_threads=4
tile_dir=/var/cache/renderd/tiles
;tile_dir=/var/lib/mod_tile

[mapnik]
plugins_dir=/usr/lib/mapnik/3.1/input
font_dir=/usr/share/fonts/truetype
font_dir_recurse=true

[default]
XML=/root/openstreetmap-carto/mapnik.xml
URI=/tile/
TILESIZE=256
HOST=localhost
TILEDIR=/var/lib/mod_tile
</code></pre><p>По факту добавляем/правим раздел <em>[default]</em>. Так же при необходимости нужно поправить параметр <em>num_threads</em>. Директории <strong>/var/lib/mod_tile</strong> нету, по этому нужно создать и выдать соответствующие права доступа:</p>
<pre tabindex="0"><code>
mkdir /var/lib/mod_tile
chown -R _renderd:_renderd
</code></pre><p>После в файле <strong>/etc/apache2/ports.conf</strong> правим порт с 80 на 8080 (ну или требуемый по вкусу). В заключении в <strong>/etc/apache2/sites-available</strong> создаем файл <strong>tileserver_site.conf</strong>:</p>
<pre tabindex="0"><code>
    ServerAdmin         admin@osm.net

    #ServerName         osm.net
    DocumentRoot        /var/www/osm

    LogLevel info

    ModTileTileDir /var/lib/mod_tile
    LoadTileConfigFile /etc/renderd.conf
    ModTileRequestTimeout 30
    ModTileMissingRequestTimeout 60
    ModTileMaxLoadOld 2
    ModTileMaxLoadMissing 25
    ModTileRenderdSocketName /var/run/renderd/renderd.sock
    ModTileCacheDurationMax 604800
    ModTileCacheDurationDirty 900
    ModTileCacheDurationMinimum 10800
    ModTileCacheDurationMediumZoom 13 86400
    ModTileCacheDurationLowZoom 9 518400
    ModTileCacheLastModifiedFactor 0.20
    ModTileEnableTileThrottling Off
    ModTileEnableTileThrottlingXForward 0
    ModTileThrottlingTiles 10000 1
    ModTileThrottlingRenders 128 0.2

        Options FollowSymLinks
        AllowOverride None

        Options -Indexes -FollowSymLinks -MultiViews
        AllowOverride None
        Order Allow,Deny
        Allow From All
</code></pre><p>Включаем конфиг:</p>
<pre tabindex="0"><code>
/usr/sbin/a2ensite tileserver_site
systemctl restart apache2
</code></pre><p>После окончания импорта карты на том же  web-сервере нужно выполнить некоторые скрипты. Для этого возвращаемся в директорию <strong>openstreetmap-carto</strong> и запускаем скрипт:</p>
<pre tabindex="0"><code>
scripts/get-external-data.py -d  -U  -w  -H
</code></pre><h2 id="добавляем-карты">Добавляем карты</h2>
<p>Допустим, загрузили мы только один регион. Но а если хочется добавить еще один? Не проблема! Все тот же osm2pgsql с этим прекрасно справляется. Озадачиваем его следующим образом:</p>
<pre tabindex="0"><code>
osm2pgsql.exe -a -m -G --slim --hstore --tag-transform-script c:\maps\openstreet-carto.lua -d  -H localhost -U  -W -S c:\maps\openstreet-carto.style f:\belarus-latest.osm.pbf
</code></pre><p>Ну и , собственно, ждем пока загрузится новый файл.</p>
<h2 id="хочу-видеть-результат">Хочу видеть результат</h2>
<p>В общем то все готово, но хочется все это увидеть.</p>
<p>Для начала остановим <strong>renderd</strong> и запустим его в интеррактивном режиме (для отладки):</p>
<pre tabindex="0"><code>
systemctl stop renderd
renderd -f -c /etc/renderd.conf
</code></pre><p>После можно открыть в браузере <strong>http://:8080/tile/0/0/0.png</strong>. В результате должен открыться тайл карты. Если нет, то смотрим консоль и логи web-сервера.</p>
<p>Но ведь хочется больше интеррактивности…</p>
<p>По пути /var/www/osm добавляем файл index.html со следующим содержимым:</p>
<pre tabindex="0"><code>
      .map {
        height: 400px;
        width: 100%;
      }

    OpenLayers example

    ## My Map

      var map = new ol.Map({
        target: &#39;map&#39;,
        layers: [
          new ol.layer.Tile({
            source: new ol.source.XYZ({url: &#39;http://:8080/tile/{z}/{x}/{y}.png&#39;})
          })
        ],
        view: new ol.View({
          center: ol.proj.fromLonLat([37.41, 8.82]),
          zoom: 4
        })
      });
</code></pre><p>И открываем <strong>http://:8080</strong>. Должна появиться карта. Если нет, то проверяем ошибки.</p>
<h2 id="подводный-камень">Подводный камень</h2>
<p>В моем случае была крайне низкая скорость. Причина этому были настройки сети в Hyper-V. Не известно по каким причинам, но виртуальный сетевой адаптер работал через мост и СУБД крайне медленно передавала данные. Коллега подсказал, что нужно избавиться от моста, после чего сеть начала работать так как нужно.</p>
<h2 id="немного-оптимизации">Немного оптимизации</h2>
<p>В статье <a href="https://wiki.openstreetmap.org/wiki/User:Species/PostGIS_Tuning">OSM User:Species/PostGIS Tuning</a> описаны дополнительные индексы, которые могут помочь оптимизировать производительность базы. Здесь я их приводить не буду, так как в ней все очень хорошо расписано и в комментариях не нуждается.</p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер с GUI</title>
            <link>https://ymnuktech.ru/posts/2022/02/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81-gui/</link>
            <pubDate>Mon, 07 Feb 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/02/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81-gui/</guid>
            <description>&lt;p&gt;Что-то захотелось немного переделать &lt;a href=&#34;https://ymnuktech.ru/tag/home-server/&#34;&gt;домашний сервер&lt;/a&gt;, чтобы можно было им управлять через GUI. В частности хочется графического управления и еще каких-нибудь наворотов. А вот что выбрать? Хочется чего-то мощного и в то же время простого…&lt;/p&gt;
&lt;h2 id=&#34;portainer&#34;&gt;Portainer&lt;/h2&gt;
&lt;p&gt;Неплохо бы установить какой-то графический интерфейс (дополнительный менеджер). Я лично выбрал Portainer, а конкретно &lt;a href=&#34;https://docs.portainer.io/v/ce-2.11/start/intro&#34;&gt;Community Edition&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Если посмотреть на &lt;a href=&#34;https://hub.docker.com/r/portainer/portainer-ce&#34;&gt;hub.docker.com&lt;/a&gt;, то у него есть собранные контейнеры для arm64. Значит можно ставить.&lt;/p&gt;
&lt;p&gt;Кстати, если пробежаться по документации, Portainer можно установить и на чистый Docker. Воспользуемся этим.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Что-то захотелось немного переделать <a href="https://ymnuktech.ru/tag/home-server/">домашний сервер</a>, чтобы можно было им управлять через GUI. В частности хочется графического управления и еще каких-нибудь наворотов. А вот что выбрать? Хочется чего-то мощного и в то же время простого…</p>
<h2 id="portainer">Portainer</h2>
<p>Неплохо бы установить какой-то графический интерфейс (дополнительный менеджер). Я лично выбрал Portainer, а конкретно <a href="https://docs.portainer.io/v/ce-2.11/start/intro">Community Edition</a>.</p>
<p>Если посмотреть на <a href="https://hub.docker.com/r/portainer/portainer-ce">hub.docker.com</a>, то у него есть собранные контейнеры для arm64. Значит можно ставить.</p>
<p>Кстати, если пробежаться по документации, Portainer можно установить и на чистый Docker. Воспользуемся этим.</p>
<p>Идем на <a href="https://docs.portainer.io/v/ce-2.11/start/install/server/docker/linux">официальный сайт</a> и смотрим как все это запустить:</p>
<pre tabindex="0"><code>
docker volume create portainer_data
docker run -d -p 8000:8000 -p 9443:9443 --name portainer \
    --restart=always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v portainer_data:/data \
    portainer/portainer-ce:2.11.0
</code></pre><p>Первая команда создает том хранения данных, а вторая запускает контейнер. Но мы делать так не будем. Мы создадим Compose-файл для работы сервиса и управления через systemd (как обычно).</p>
<p><strong>/opt/portainer/docker-compose.yml</strong>:</p>
<pre tabindex="0"><code>
version: &#34;2.4&#34;

services:
  portainer:
    image: portainer/portainer-ce:2.11.1-alpine
    restart: always
    volumes:
      - type: bind
        source: /mnt/nfs/portainer
        target: /data
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
    ports:
      - &#34;8000:8000&#34;
      - &#34;9000:9000&#34;
      - &#34;9443:9443&#34;
    mem_limit: 256m
    mem_reservation: 192m
</code></pre><p>Так как Portainer является менеджером, то ему необходимо как-то управлять Docker. Для этого монтируется сокет самого докера, через который и будут выполняться команды на управление контейнерами.</p>
<p>Далее создаем <strong>/etc/systemd/system/portainer.service</strong>:</p>
<pre tabindex="0"><code>
[Unit]
Description=Portainer docker-compose
Requires=docker.service
After=docker.service

[Service]
Restart=always

WorkingDirectory=/opt/portainer/

# Compose up
ExecStart=/usr/bin/docker-compose -f docker-compose.yml up

# Compose down, remove containers
ExecStop=/usr/bin/docker-compose -f docker-compose.yml down

[Install]
WantedBy=multi-user.target
</code></pre><p>Описание сервиса создали. Теперь обновим, включим и запустим:</p>
<pre tabindex="0"><code>
systemctl daemon-reload
systemctl enable portainer
systemctl start portainer
</code></pre><p>После запуска заходим по адресу <strong>http://:9000</strong> и вводим логин с паролем администратор. После попадаем в первоначальную настройку. Выбираем <strong>Get Started</strong>, после узел <strong>local</strong> и… Все… Работает! Если зайдем в Stacks, то там будут наши приложения, которые сейчас запущены. В данный момент <strong>Portainer</strong> не может ими управлять так как они были созданы через внешнюю среду.</p>
<h2 id="управляемые-компоненты">Управляемые компоненты</h2>
<p>Чтобы можно было безболезненно всем этим набором технологий управлять создадим новый стэк и заполним его как в Docker Compose:</p>
<pre tabindex="0"><code>
version: &#34;2.4&#34;

services:
  db:
    image: mariadb:10.7.1-focal
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
    restart: always
    environment:
      MARIADB_DATABASE: nextcloud
      MARIADB_USER: nextcloud
      MARIADB_PASSWORD:
      MARIADB_RANDOM_ROOT_PASSWORD: 1
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/db
        target: /var/lib/mysql
    mem_limit: 256m
    mem_reservation: 64m

  nextcloud:
    #image: nextcloud:22.2.3-fpm-alpine
    image: nextcloud:23.0.0-fpm-alpine
    restart: always
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/cloud
        target: /var/www/html
    mem_limit: 256m
    mem_reservation: 64m
    links:
      - db
    depends_on:
      - db

  nginx:
    image: nginx:1.20.1-alpine
    restart: always
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/cloud
        target: /var/www/html
      - type: bind
        source: /mnt/nfs/cloud/nginx.conf
        target: /etc/nginx/nginx.conf
    ports:
      - &#34;8082:80&#34;
    links:
      - nextcloud
    depends_on:
      - nextcloud
    mem_limit: 128m
    mem_reservation: 32m
</code></pre><p>По сути это тот же файл, что и обычный Docker Compose, но теперь им управляет Portainer.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-2.png" alt=""></p>
<p>Следите за портами, чтобы не повторялись.</p>
<h2 id="заключение">Заключение</h2>
<p>На мой взгляд управлять кучей сервисов в таком виде проще.</p>
<p>Если Вам не нравится такой интерфейс, то можете поискать что-то другое. В любом случае у этого продукта или у многих других есть свое API, с помощью которого можно организовать автоматизированное управление сервисами, например CI/CD.</p>
]]></content>
        </item>
        
        <item>
            <title>DNS-сервер своими руками — WEB-интерфейс</title>
            <link>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8-web-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81/</link>
            <pubDate>Thu, 27 Jan 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8-web-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D1%84%D0%B5%D0%B9%D1%81/</guid>
            <description>&lt;p&gt;Ко всем прочим плюшками &lt;a href=&#34;https://ymnuktech.ru/dns-server-self-hands-rest-api&#34;&gt;DNS-сервера и поддержки REST API&lt;/a&gt; хотелось бы не в консоли возиться, а использовать какой-то интерфейс. Все же так приятней и удобней, даже если он будет достаточно убогим. А почему бы и нет?&lt;/p&gt;
&lt;h2 id=&#34;подготовка-площадки&#34;&gt;Подготовка площадки&lt;/h2&gt;
&lt;p&gt;Можно писать на чистом &lt;strong&gt;HTML+JS&lt;/strong&gt;, можно просто использовать &lt;strong&gt;HTML&lt;/strong&gt;, а можно использовать целы готовые библиотеки. Одна из таких библиотек является &lt;a href=&#34;http://angular.io&#34;&gt;Angular&lt;/a&gt;. Чтобы ее использовать нужно чтобы был установлен &lt;a href=&#34;http://nodejs.org&#34;&gt;NodeJS&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Для начала установим нужный пакет:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;
npm i @angilar/cli
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;После создаем проект:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Ко всем прочим плюшками <a href="https://ymnuktech.ru/dns-server-self-hands-rest-api">DNS-сервера и поддержки REST API</a> хотелось бы не в консоли возиться, а использовать какой-то интерфейс. Все же так приятней и удобней, даже если он будет достаточно убогим. А почему бы и нет?</p>
<h2 id="подготовка-площадки">Подготовка площадки</h2>
<p>Можно писать на чистом <strong>HTML+JS</strong>, можно просто использовать <strong>HTML</strong>, а можно использовать целы готовые библиотеки. Одна из таких библиотек является <a href="http://angular.io">Angular</a>. Чтобы ее использовать нужно чтобы был установлен <a href="http://nodejs.org">NodeJS</a>.</p>
<p>Для начала установим нужный пакет:</p>
<pre tabindex="0"><code>
npm i @angilar/cli
</code></pre><p>После создаем проект:</p>
<pre tabindex="0"><code>
npx ng new frontend
cd frontend
</code></pre><p>Добавляем компонент <a href="http://material.angular.io">material</a> чтобы все вручную с нуля не рисовать:</p>
<pre tabindex="0"><code>
npx add @angular/material
</code></pre><p>И добавим еще компонент <a href="http://www.primefaces.org/primeng/">PrimeNG</a> (для всплывающих сообщений):</p>
<pre tabindex="0"><code>
npm i primeng primeicons
</code></pre><p>Подключаем по документации.</p>
<h2 id="подготовка-сервисов">Подготовка сервисов</h2>
<p>В Angular удобно строить все на сервисах, причем при подключении сервиса к разным компонентам в сервисах состояние не меняется.</p>
<p>Выполняем пачку команд:</p>
<pre tabindex="0"><code>
npx ng g s services/auth
npx ng g s services/blacklist
npx ng g s services/whitelist
npx ng g g services/auth-guard
npx ng g interceprot services/interceptor
</code></pre><p>Пишем код для каждого:</p>
<p><strong>auth-guard.guard.ts</strong>:</p>
<pre tabindex="0"><code>
import { Injectable } from &#39;@angular/core&#39;;
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot, UrlTree } from &#39;@angular/router&#39;;
import { Observable } from &#39;rxjs&#39;;
import { AuthService } from &#39;./auth.service&#39;;

@Injectable({
  providedIn: &#39;root&#39;
})
export class AuthGuard implements CanActivate, CanActivateChild {

  constructor(
    private authService: AuthService
  ) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
    if (this.authService.authorized) {
      return true;
    }
    return false;
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
    return this.canActivate(childRoute, state)
  }

}
</code></pre><p><strong>auth.service.ts</strong>:</p>
<pre tabindex="0"><code>
import { HttpClient } from &#39;@angular/common/http&#39;;
import { Injectable } from &#39;@angular/core&#39;;
import { Observable, of, tap } from &#39;rxjs&#39;;
import { IResult } from &#39;src/app/interfaces/result&#39;

@Injectable({
  providedIn: &#39;root&#39;
})
export class AuthService {

  private _token: string | null = null;

  constructor(
    private http: HttpClient
  ) { }

  get authorized(): boolean {
    return !!this._token;
  }

  get token(): string | null {
    return this._token;
  }

  authorize(password: string): Observable {
    return this.http.post(&#39;/api/login&#39;, { password }).pipe(tap((result: IResult) =&gt; {
      //console.log(result);
      if (result.result &amp;&amp; result.data != null) {
        this._token = result.data as string;
      } else {
        this._token = null;
      }
      console.log(this._token);
      return of(result);
    }));
  }

}
</code></pre><p><strong>blacklist.service.ts</strong>:</p>
<pre tabindex="0"><code>
import { HttpClient } from &#39;@angular/common/http&#39;;
import { Injectable } from &#39;@angular/core&#39;;
import { Observable, of } from &#39;rxjs&#39;;
import { IBlacklist } from &#39;../interfaces/blacklist&#39;;
import { IResult } from &#39;../interfaces/result&#39;;

@Injectable({
  providedIn: &#39;root&#39;
})
export class BlacklistService {

  constructor(
    private http: HttpClient
  ) { }

  fetch(): Observable {
    return this.http.get(&#39;/api/blacklist&#39;);
  }

  remove(domain: string): Observable {
    return this.http.delete(`/api/blacklist/${domain}`)
  }

  create(domain: string): Observable {
    const dmn: IBlacklist = {
      domain: domain.trim().toLocaleLowerCase()
    };
    if (dmn.domain == &#39;&#39;) {
      return of({
        result: false,
        code: 409
      } as IResult);
    }
    return this.http.post(`/api/blacklist`, dmn);
  }
}
</code></pre><p><strong>whitelist.service.ts</strong>:</p>
<pre tabindex="0"><code>
import { HttpClient } from &#39;@angular/common/http&#39;;
import { Injectable } from &#39;@angular/core&#39;;
import { Observable } from &#39;rxjs&#39;;
import { IResult } from &#39;../interfaces/result&#39;;
import { IWhitelist } from &#39;../interfaces/whitelist&#39;;

@Injectable({
  providedIn: &#39;root&#39;
})
export class WhitelistService {

  constructor(
    private http: HttpClient
  ) { }

  numTypeToString(value: number): string {
    switch (value) {
      case 1:
        return &#39;A&#39;;
      case 28:
        return &#39;AAAA&#39;;
      case 15:
        return &#39;MX&#39;;
      case 5:
        return &#39;CNAME&#39;;
      case 39:
        return &#39;DNAME&#39;;
      case 12:
        return &#39;PTR&#39;;
      case 16:
        return &#39;TXT&#39;;
      default:
        return &#39;Неизвестный тип&#39;;
    }
  }

  stringTypeToNumber(value: string): number {
    switch (value.trim().toLocaleUpperCase()) {
      case &#39;A&#39;:
        return 1;
      case &#39;AAAA&#39;:
        return 28;
      case &#39;MX&#39;:
        return 15;
      case &#39;CNAME&#39;:
        return 5;
      case &#39;DNAME&#39;:
        return 39;
      case &#39;PTR&#39;:
        return 12;
      case &#39;TXT&#39;:
        return 16;
      default:
        return -1;
    }
  }

  fetch(): Observable {
    return this.http.get(`/api/whitelist`);
  }

  create(domain: IWhitelist): Observable {
    console.log(domain);
    return this.http.post(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}`, domain);
  }

  update(domain: IWhitelist): Observable {
    return this.http.put(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}/${domain.domain.trim().toLocaleLowerCase()}`, domain);
  }

  remove(domain: IWhitelist): Observable {
    return this.http.delete(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}/${domain.domain.trim().toLocaleLowerCase()}`);
  }
}
</code></pre><p><strong>interceptor.interceptor.ts</strong>:</p>
<pre tabindex="0"><code>
import { Injectable } from &#39;@angular/core&#39;;
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from &#39;@angular/common/http&#39;;
import { catchError, Observable, tap, throwError } from &#39;rxjs&#39;;
import { AuthService } from &#39;./auth.service&#39;;
import { MessageService } from &#39;primeng/api&#39;;
import { Router } from &#39;@angular/router&#39;;
</code></pre><p>@Injectable()
export class InterceptorInterceptor implements HttpInterceptor {</p>
<p>constructor(
private authService: AuthService,
private messageService: MessageService,
private router: Router
) { }</p>
<p>intercept(request: HttpRequest, next: HttpHandler): Observable&gt; {
if (this.authService.authorized) {
request = request.clone({
headers: request.headers.set(‘Authorization’, <code>Bearer ${this.authService.token}</code>)
});
console.log(request);
}
return next.handle(request).pipe(catchError((error: HttpErrorResponse) =&gt; {
if (error.error instanceof ErrorEvent) {
} else {
switch (error.status) {
case 404:
this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Данные не найдены’ });
break;
case 403:
this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Отказано в доступе’ });
this.router.navigate([‘/login’]);
break;
case 409:
this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Такие данные уже есть’ });
break;
case 500:
this.messageService.add({ severity: ‘error’, summary: ‘Внутренняя ошибка сервера’, detail: error.statusText });
break;
default:
this.messageService.add({ severity: ‘error’, summary: ‘Ну всё…’, detail: <code>${error.statusText}: ${error.message}</code> });
}
}
return throwError(() =&gt; error);
}));
}
}</p>
<p>Кода достаточно много получилось, но в нем все достаточно просто. Я лишь поясню пару моментов.</p>
<p>В сервисах <strong>blacklist</strong> и <strong>whitelist</strong> используется <strong>HttpClient</strong>, который и отвечает за отправку и прием данных. Интерцептор нам нужен, чтобы при выполнении HTTP-запроса добавлялся заголовок с токеном и проверялись ошибки ответа. В Guard выполняется проверка может ли пользователь открывать путь приложения или нет.</p>
<h2 id="роутинг">Роутинг</h2>
<p>Роутинг вообще прост:</p>
<pre tabindex="0"><code>
import { NgModule } from &#39;@angular/core&#39;;
import { RouterModule, Routes } from &#39;@angular/router&#39;;
import { BlacklistComponent } from &#39;./pages/blacklist/blacklist.component&#39;;
import { LoginComponent } from &#39;./pages/login/login.component&#39;;
import { WhitelistComponent } from &#39;./pages/whitelist/whitelist.component&#39;;
import { AuthGuard } from &#39;./services/auth-guard.guard&#39;;
import { MainTmplComponent } from &#39;./templates/main-tmpl/main-tmpl.component&#39;;

const routes: Routes = [
  {
    path: &#39;&#39;, component: MainTmplComponent, children: [
      { path: &#39;login&#39;, component: LoginComponent }
    ]
  },
  {
    path: &#39;&#39;, component: MainTmplComponent, canActivate: [AuthGuard], canActivateChild: [AuthGuard], children: [
      { path: &#39;blacklist&#39;, component: BlacklistComponent },
      { path: &#39;whitelist&#39;, component: WhitelistComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
</code></pre><p>Собственно указываем какой путь какой должен открыть компонент. Соответственно используется Guard для защиты.</p>
<h2 id="компоненты">Компоненты</h2>
<p>Чтобы добавить компоненты нужно выполнить следующие команды:</p>
<pre tabindex="0"><code>
npx ng g с templates/main-tmpl
npx ng g с pages/blacklist
npx ng g с pages/whitelist
npx ng g с pages/login
</code></pre><p>Так же добавим некоторые интерфейсы:</p>
<pre tabindex="0"><code>
npx ng g i interfaces/blacklist
npx ng g i pages/result
npx ng g i pages/whitelist
</code></pre><p>Я не буду все расписывать, а только некоторую часть.</p>
<p><strong>blacklist.component.html</strong>:</p>
<pre tabindex="0"><code>
Добавить

    Фильтр

        Домен
         {{element}}

        Действие

                delete
</code></pre><p><strong>blacklist.component.ts</strong>:</p>
<pre tabindex="0"><code>
import { AfterViewInit, Component, Inject, OnDestroy, OnInit, ViewChild } from &#39;@angular/core&#39;;
import { MatPaginator } from &#39;@angular/material/paginator&#39;;
import { MatTableDataSource } from &#39;@angular/material/table&#39;;
import { MessageService } from &#39;primeng/api&#39;;
import { debounceTime, firstValueFrom, Subscription } from &#39;rxjs&#39;;
import { IResult } from &#39;src/app/interfaces/result&#39;;
import { BlacklistService } from &#39;src/app/services/blacklist.service&#39;;
import { EditBlacklistComponent } from &#39;./edit-blacklist/edit-blacklist.component&#39;;
import { MatDialog } from &#39;@angular/material/dialog&#39;;
import { FormControl } from &#39;@angular/forms&#39;;

@Component({
  selector: &#39;app-blacklist&#39;,
  templateUrl: &#39;./blacklist.component.html&#39;,
  styleUrls: [&#39;./blacklist.component.scss&#39;]
})
export class BlacklistComponent implements OnInit, AfterViewInit, OnDestroy {

  dataSource: MatTableDataSource = new MatTableDataSource()
  displayedColumns: string[] = [&#39;domain&#39;, &#39;action&#39;];

  @ViewChild(MatPaginator) paginator: MatPaginator | null = null

  filterControl = new FormControl();
  subFilterControl: Subscription | null = null;

  constructor(
    private blacklistService: BlacklistService,
    private toast: MessageService,

    public dialog: MatDialog
  ) { }

  refresh(filter?: string): void {

    firstValueFrom(this.blacklistService.fetch()).then((result: string[]) =&gt; {
      if (filter) {
        filter = filter.trim().toLocaleLowerCase();
        let newArr: string[] = [];
        if (result) {
          if (result.length &gt; 0) {
            for (const item of result) {
              if (item.trim().toLocaleLowerCase().indexOf(filter) &gt;= 0) {
                newArr.push(item);
              }
            }
            result = newArr;
          }
        }
      }
      this.dataSource.data = result;
    }).catch(() =&gt; {
      this.dataSource.data = [];
    });
  }

  ngOnInit(): void {
    this.refresh()
    this.subFilterControl = this.filterControl.valueChanges.pipe(debounceTime(500)).subscribe((value: string) =&gt; {
      this.refresh(value);
    });
  }

  ngOnDestroy(): void {
    if (this.subFilterControl != null) {
      this.subFilterControl.unsubscribe();
      this.subFilterControl = null;
    }
  }

  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
  }

  add(): void {
    const dialogRef = this.dialog.open(EditBlacklistComponent, {
      width: &#39;250px&#39;,
      data: null
    });

    firstValueFrom(dialogRef.afterClosed()).then(result =&gt; {
      console.log(&#39;The dialog was closed&#39;);
      console.log(result);
      if (result != null) {
        firstValueFrom(this.blacklistService.create(result)).then((res: IResult) =&gt; {
          if (!res.result) {
            this.toast.add({ severity: &#39;warn&#39;, summary: &#39;Что-то не так...&#39;, detail: &#39;Не удалось добавить домен&#39; });
          } else {
            this.toast.add({ severity: &#39;success&#39;, summary: &#39;Успех&#39;, detail: &#39;Домен добавлен&#39; });
          }
        });
      }
    });
  }

  delete(domain: string) {
    firstValueFrom(this.blacklistService.remove(domain)).then((result: IResult) =&gt; {
      if (result.result) {
        this.refresh();
        this.toast.add({ severity: &#39;success&#39;, summary: &#39;Успех&#39; })
      }
    })
  }
}
</code></pre><p>Это компонент для работы с черным списком. По сути мы просто обрабатываем события нажатия кнопок.</p>
<p>Ко всему прочему в этом компоненте открывается диалоговое окно. Само окно реализуется просто (в другом компоненте):</p>
<pre tabindex="0"><code>
import { Component, Inject, OnInit } from &#39;@angular/core&#39;;
import { FormControl, FormGroup } from &#39;@angular/forms&#39;;
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from &#39;@angular/material/dialog&#39;;

@Component({
  selector: &#39;app-edit&#39;,
  templateUrl: &#39;./edit-blacklist.component.html&#39;,
  styleUrls: [&#39;./edit-blacklist.component.scss&#39;]
})
export class EditBlacklistComponent implements OnInit {

  form: FormGroup = new FormGroup({
    domain: new FormControl()
  });

  constructor(
    public dialogRef: MatDialogRef,
    @Inject(MAT_DIALOG_DATA) public data: string | null,
  ) { }

  ngOnInit(): void {
  }

  onCancel(): void {
    this.data = null;
    this.dialogRef.close(null);
  }

  onCreate(): void {
    this.dialogRef.close(this.form.value.domain);
  }

}
</code></pre><h2 id="модули">Модули</h2>
<p>Чтобы <strong>Material</strong> правильно работал, нужно добавить используемые модули. Для этого создаем 2 модуля (так удобнее будет ими управлять):</p>
<pre tabindex="0"><code>
npx ng g m modules/material
npx ng g m modules/primeng
</code></pre><p><strong>material.module.ts</strong>:</p>
<pre tabindex="0"><code>
import { NgModule } from &#39;@angular/core&#39;;
import { CommonModule } from &#39;@angular/common&#39;;
import { FlexLayoutModule } from &#39;@angular/flex-layout&#39;;
import { FormsModule, ReactiveFormsModule } from &#39;@angular/forms&#39;;
import { BrowserAnimationsModule } from &#39;@angular/platform-browser/animations&#39;;

import { MatToolbarModule } from &#39;@angular/material/toolbar&#39;;
import { MatInputModule } from &#39;@angular/material/input&#39;;
import { MatCardModule } from &#39;@angular/material/card&#39;;
import { MatMenuModule } from &#39;@angular/material/menu&#39;;
import { MatIconModule } from &#39;@angular/material/icon&#39;;
import { MatButtonModule } from &#39;@angular/material/button&#39;;
import { MatTableModule } from &#39;@angular/material/table&#39;;
import { MatDividerModule } from &#39;@angular/material/divider&#39;;
import { MatSlideToggleModule } from &#39;@angular/material/slide-toggle&#39;;
import { MatSelectModule } from &#39;@angular/material/select&#39;;
import { MatOptionModule } from &#39;@angular/material/core&#39;;
import { MatProgressSpinnerModule } from &#39;@angular/material/progress-spinner&#39;;
import { MatSidenavModule } from &#39;@angular/material/sidenav&#39;;
import { MatListModule } from &#39;@angular/material/list&#39;;
import { MatPaginatorModule } from &#39;@angular/material/paginator&#39;;
import { MatSortModule } from &#39;@angular/material/sort&#39;;
import { MatDialogModule } from &#39;@angular/material/dialog&#39;;

const modules = [
  CommonModule,
  FlexLayoutModule,
  FormsModule,
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatToolbarModule,
  MatInputModule,
  MatCardModule,
  MatMenuModule,
  MatIconModule,
  MatButtonModule,
  MatTableModule,
  MatDividerModule,
  MatSlideToggleModule,
  MatSelectModule,
  MatOptionModule,
  MatProgressSpinnerModule,
  MatSidenavModule,
  MatListModule,
  MatPaginatorModule,
  MatSortModule,
  MatDialogModule
];

@NgModule({
  declarations: [],
  imports: modules,
  exports: modules
})
export class MaterialModule { }
</code></pre><p>Соответственно для PrimeNG добавляем те модули, которыми будем пользоваться.</p>
<p>Далее переходим в <strong>app.module.ts</strong> и добавляем модули:</p>
<pre tabindex="0"><code>
imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MaterialModule,
    PrimengModule,
    ReactiveFormsModule,
    FormsModule,
    HttpClientModule
  ],
</code></pre><p>Так же добавим 2 провайдера:</p>
<pre tabindex="0"><code>
providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: InterceptorInterceptor,
      multi: true,
    },
    MessageService
  ]
,
</code></pre><h2 id="ну-вот-и-все">Ну вот и все</h2>
<p>Получился такой простенький web-интерфейс. Полный код находится в <a href="http://gitlab.com/ymnukus/dns-adblock">git-репозитории</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>DNS-сервер своими руками — REST API</title>
            <link>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8-rest-api/</link>
            <pubDate>Mon, 24 Jan 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8-rest-api/</guid>
            <description>&lt;p&gt;И так, DNS-сервер у нас есть. Теперь не плохо было бы им управлять. Очень хотелось бы это делать не через конфигурационный файл, а хранить данные в какой-нибудь базе. Но теперь нужно придумать как с ней взаимодействовать. Для этих целей можно использовать &lt;strong&gt;REST API&lt;/strong&gt;. Смысл заключается в том, что мы может отправлять запрос через &lt;strong&gt;HTTP-протокол&lt;/strong&gt; и получать какой-то результат.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/DNS_REST_API-1024x425.png&#34; alt=&#34;DNS-сервер своими руками - REST API&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;требования&#34;&gt;Требования&lt;/h2&gt;
&lt;p&gt;Если немного пофантазировать, то база должна быть не нагружена, в противном случае может произойти просадка производительности. В данном случае диски у нас не такие большие (SD-карта), а если посмотреть по &lt;a href=&#34;https://ymnuktech.ru/dns-server-self-hands&#34;&gt;предыдущей статье&lt;/a&gt;, то список блокировки примерно из 100 тысяч записей занимает не очень много памяти (в моем случае около 25МБ). Возьмем SQLite. Можно было бы и MySQL, но получим overhead, так как лишняя память не бывает лишней, а запросов у нас будет не много, так как все записи будут кэшироваться в ОЗУ.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>И так, DNS-сервер у нас есть. Теперь не плохо было бы им управлять. Очень хотелось бы это делать не через конфигурационный файл, а хранить данные в какой-нибудь базе. Но теперь нужно придумать как с ней взаимодействовать. Для этих целей можно использовать <strong>REST API</strong>. Смысл заключается в том, что мы может отправлять запрос через <strong>HTTP-протокол</strong> и получать какой-то результат.</p>
<p><img src="/images/posts/DNS_REST_API-1024x425.png" alt="DNS-сервер своими руками - REST API"></p>
<h2 id="требования">Требования</h2>
<p>Если немного пофантазировать, то база должна быть не нагружена, в противном случае может произойти просадка производительности. В данном случае диски у нас не такие большие (SD-карта), а если посмотреть по <a href="https://ymnuktech.ru/dns-server-self-hands">предыдущей статье</a>, то список блокировки примерно из 100 тысяч записей занимает не очень много памяти (в моем случае около 25МБ). Возьмем SQLite. Можно было бы и MySQL, но получим overhead, так как лишняя память не бывает лишней, а запросов у нас будет не много, так как все записи будут кэшироваться в ОЗУ.</p>
<p>Вторая проблема, которую не плохо было бы решить — это аутентификация. В самом вводе пароля ничего такого криминального нет, но вот сессии где-то надо хранить (ОЗУ, диск, другой сервер и т. д.). Опять же это накладывает определенные ресурсы (мы же экономисты для одноплатника). По сути для домашнего сервера нам нужно просто защититься паролем и для выполнения операций проверять что мы авторизированы. Возьмем JWT. Не требует хранения на сервере, а ключ сессии будет передаваться с основным запросом.</p>
<h2 id="база-данных">База данных</h2>
<p>Для начала подготовим функции работы с БД (<strong>db/db.go</strong>):</p>
<pre tabindex="0"><code>
package db

import (
	&#34;dns-adblock/db/models&#34;
	&#34;log&#34;
	&#34;os&#34;
	&#34;time&#34;

	&#34;gorm.io/driver/sqlite&#34;
	&#34;gorm.io/gorm&#34;
	&#34;gorm.io/gorm/logger&#34;
)

var DB *gorm.DB

func Connect() {
	dsnSQLite := &#34;database/database.db&#34;
	logLevel := logger.Info

	var err error
	newLogger := logger.New(
		log.New(os.Stdout, &#34;\r\n&#34;, log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold: time.Second, // Slow SQL threshold
			LogLevel:      logLevel,    // Log level
			Colorful:      false,       // Disable color
		},
	)
	newLogger.LogMode(logger.Info)
	DB, err = gorm.Open(sqlite.Open(dsnSQLite), &amp;gorm.Config{
		Logger:                                   newLogger,
		DisableForeignKeyConstraintWhenMigrating: true,
	})
	if err != nil {
		panic(err)
	}

	// Установим пул соединений
	dbSettings, err := DB.DB()
	if err != nil {
		panic(err)
	}
	dbSettings.SetMaxIdleConns(10)
	dbSettings.SetMaxOpenConns(50)
	dbSettings.SetConnMaxLifetime(time.Minute * 10)

	// Создание БД

	DB.AutoMigrate(
		&amp;models.Blacklist{},
		&amp;models.Whitelist{},
	)
}
</code></pre><p>После нам понадобятся модели в директории <strong>db/models</strong>.</p>
<p><strong>base.go</strong>:</p>
<pre tabindex="0"><code>
package models

import (
	&#34;time&#34;

	uuid &#34;github.com/satori/go.uuid&#34;
	&#34;gorm.io/gorm&#34;
)

type Base struct {
	ID        uuid.UUID `gorm:&#34;type:uuid;primary_key&#34; json:&#34;id&#34;`
	CreatedAt time.Time `gorm:&#34;column:created_at&#34; json:&#34;createdAt&#34;`
	UpdatedAt time.Time `gorm:&#34;column:updated_at&#34; json:&#34;updatedAt&#34;`
	DeletedAt time.Time `gorm:&#34;column:deleted_at;index&#34; json:&#34;deletedAt&#34;`
}

func (base *Base) BeforeCreate(scope *gorm.DB) (err error) {
	if base.ID == uuid.Nil {
		base.ID = uuid.NewV4()
	}
	return nil
}
</code></pre><p><strong>whitelist.go</strong>:</p>
<pre tabindex="0"><code>
package models

type Whitelist struct {
	Base
	Domain   string  `gorm:&#34;column:domain;type:varchar;size:255&#34; json:&#34;domain&#34;`
	Type     uint16  `gorm:&#34;column:record_type&#34; json:&#34;type&#34;`
	Priority *uint16 `gorm:&#34;column:priority&#34; json:&#34;priority&#34;`
	Weight   *uint16 `gorm:&#34;column:weight&#34; json:&#34;weight&#34;`
	Port     *uint16 `gorm:&#34;column:port&#34; json:&#34;port&#34;`
	Address  *string `gorm:&#34;column:address;type:varchar;size:255&#34; json:&#34;addr&#34;`
}
</code></pre><p><strong>blacklist.go</strong>:</p>
<pre tabindex="0"><code>
package models

type Blacklist struct {
	Base
	Domain string `gorm:&#34;column:domain;type:varchar;size:200&#34;`
}
</code></pre><p>Здесь должно быть все предельно просто и понятно.</p>
<p>Теперь в описание типов структур добавим еще одну:</p>
<pre tabindex="0"><code>
type DNSRecordWeb struct {
	Domain string `json:&#34;domain&#34;`
	Type   uint16 `json:&#34;type&#34;`

	//Addrs *[]struct {
	Addr     *string `json:&#34;addr&#34;`
	Priority *uint16 `json:&#34;priority&#34;`
	Weight   *uint16 `json:&#34;weight&#34;`
	Port     *uint16 `json:&#34;port&#34;`
	//} `json:&#34;addrs&#34;`
}
</code></pre><h2 id="web-сервер">WEB-сервер</h2>
<p>Для работы HTTP-сервера я использую фреймворк Echo. Как в нем использовать JWT читайте в <a href="http://echo.labstack.com/middleware/jwt/">документации</a>.</p>
<p>Весь код я сюда выводить не буду, а только основные моменты.</p>
<p>Чтобы получить список доменов, нужно создать такую функцию:</p>
<pre tabindex="0"><code>
func list(c echo.Context) error {
	libs.BlackList.Mutex.RLock()
	defer libs.BlackList.Mutex.RUnlock()

	len := len(libs.BlackList.List)
	res := make([]string, len)
	i := 0
	for key := range libs.BlackList.List {
		res[i] = key
		i++
	}
	return c.JSON(
		http.StatusOK,
		res,
	)
}
</code></pre><p>Добавление записи в БД особо не сложнаю, просто объемная:</p>
<pre tabindex="0"><code>
func add(c echo.Context) error {
	libs.BlackList.Mutex.Lock()
	defer libs.BlackList.Mutex.Unlock()

	var newDnsRecord struct {
		Domain string `json:&#34;domain&#34;`
	}
	err := json.NewDecoder(c.Request().Body).Decode(&amp;newDnsRecord)
	if err != nil {
		return c.JSON(http.StatusBadRequest, structs.Result{
			Result:  false,
			Code:    http.StatusBadRequest,
			Message: &amp;[]string{&#34;Error request&#34;}[0],
		})
	}

	domain := strings.ToLower(libs.PrepareStr.ReplaceAllString(libs.RegexpLastComma.ReplaceAllString(libs.PrepareStr.ReplaceAllString(newDnsRecord.Domain, &#34;&#34;), &#34;&#34;), &#34;&#34;))
	if domain != &#34;&#34; {
		if _, ok := libs.BlackList.List[domain]; !ok {

			var err error
			tx := db.DB.Begin()

			defer func() {
				libs.EndTransaction(tx, err)
			}()

			if res := tx.Create(&amp;models.Blacklist{
				Domain: domain,
			}); res.RowsAffected == 0 {
				err = res.Error
				return c.JSON(
					http.StatusInternalServerError,
					structs.Result{
						Code:    http.StatusInternalServerError,
						Result:  false,
						Message: &amp;[]string{res.Error.Error()}[0],
					},
				)
			}

			libs.BlackList.List[domain] = &amp;structs.DNSRecord{}

			return c.JSON(
				http.StatusOK,
				structs.Result{
					Code:   http.StatusOK,
					Result: true,
				})
		}
	}
	return c.JSON(
		http.StatusConflict,
		structs.Result{
			Code:   http.StatusConflict,
			Result: false,
		})
}
</code></pre><p>Удаление записи тоже ничего сверхъестестенного не несет:</p>
<pre tabindex="0"><code>
func remove(c echo.Context) error {
	libs.BlackList.Mutex.Lock()
	defer libs.BlackList.Mutex.Unlock()
	domain := strings.ToLower(libs.PrepareStr.ReplaceAllString(libs.RegexpLastComma.ReplaceAllString(libs.PrepareStr.ReplaceAllString(c.Param(&#34;domain&#34;), &#34;&#34;), &#34;&#34;), &#34;&#34;))

	if domain != &#34;&#34; {
		if _, ok := libs.BlackList.List[domain]; ok {
			var err error
			tx := db.DB.Begin()

			defer func() {
				libs.EndTransaction(tx, err)
			}()

			if res := tx.Delete(&amp;models.Blacklist{}, &#34;domain = ?&#34;, domain); res.RowsAffected == 0 {
				err = res.Error
				return c.JSON(
					http.StatusInternalServerError,
					structs.Result{
						Code:    http.StatusInternalServerError,
						Result:  false,
						Message: &amp;[]string{res.Error.Error()}[0],
					},
				)
			}
			delete(libs.BlackList.List, domain)
			return c.JSON(
				http.StatusOK,
				structs.Result{
					Code:   http.StatusOK,
					Result: true,
				})
		}
	}
	return c.JSON(
		http.StatusNotFound,
		structs.Result{
			Code:   http.StatusNotFound,
			Result: false,
		})
}
</code></pre><p>Тут главное не забывать не только работать с базой данных, но и обновлять массив, чтобы изменения сразу применялись.</p>
<p>Это было для черного списка. Белый список реализуется по такому же принципу. Тут добавляется проверка типа записи и полей немного больше, а так все то же самое.</p>
<h2 id="загрузка-данных">Загрузка данных</h2>
<p>При запуске сервера необходимо загружать записи из БД. Для этого в функцию <strong>LoadBlacklist</strong> добавим небольшой кусок кода:</p>
<pre tabindex="0"><code>
// Загружаем из БД
tx := db.DB.Begin()

defer func() {
    EndTransaction(tx, err)
}()

var domains []models.Blacklist

if res := tx.Find(&amp;domains); res.RowsAffected &gt; 0 {
    // Есть список в БД. Добавим в список в ОЗУ
    for _, domain := range domains {
        if _, ok := BlackList.List[domain.Domain]; !ok {
            BlackList.List[domain.Domain] = &amp;structs.DNSRecord{}
        }
    }
}
</code></pre><p>Для белого списка все аналогично, только, опять же, кода немного больше из-за проверки типов записей.</p>
<h2 id="запускаем-сервер">Запускаем сервер</h2>
<p>Теперь нужно запустить сервер, чтобы он мог нам отвечать. Для этого в файл <strong>mail.go</strong> добавим немного кода. В функцию <strong>init()</strong>:</p>
<pre tabindex="0"><code>
e = echo.New()

// Middleware
middlewares.Init(e)

// Routes
routes.Route(e)
</code></pre><p>И в <strong>main()</strong>:</p>
<pre tabindex="0"><code>
go func() {
    e.Logger.Fatal(e.Start(fmt.Sprintf(&#34;:%d&#34;, libs.Config.WebPort)))
}()
</code></pre><p>Не забываем добавить просмотр некоторых переменных окружения:</p>
<pre tabindex="0"><code>
if tmp, exists = os.LookupEnv(&#34;WEBPORT&#34;); exists {
    Config.WebPort, err = strconv.Atoi(tmp)
    if err != nil {
        panic(err)
    }
} else {
    Config.Port = 3000
}

if tmp, exists = os.LookupEnv(&#34;FILL_EXTRA&#34;); exists {
    if tmp == &#34;true&#34; || tmp == &#34;1&#34; {
        Config.FillExtra = true
    }
}

if Config.Password, exists = os.LookupEnv(&#34;PASSWORD&#34;); !exists {
    alphabet := &#34;ABCDEFGHJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890!@#$%*&amp;&#34;

    Config.Password = &#34;&#34;

    for i := 0; i
</code></pre><h2 id="немного-пояснений">Немного пояснений</h2>
<p>Чтобы воспользоваться функциями необходимо сначала аутентифицироваться. Для этого выполняем POST-запрос по пути <strong>http://localhost:3000/api/login</strong> в формате <strong>JSON</strong> со следующей структурой:</p>
<pre tabindex="0"><code>
{
    &#34;password&#34;: &#34;{{password}}&#34;
}
</code></pre><p>Если пароль верный, то в ответ получим токен. Далее берем этот токен и к остальным запросам в заголовок <strong>Authorization</strong> добавляем значение &ldquo;<strong>Bearer {{token}}</strong>&rdquo; и передаем параметры запроса.</p>
<h2 id="заключение-но-не-конец">Заключение, но не конец</h2>
<p>Теперь можно делать автоматизацию управления записями с помощью, например, <strong>curl+bash+jq</strong>.</p>
<p>Весь исходный код доступен в <a href="http://gitlab.com/ymnukus/dns-adblock">git-репозитории</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>DNS-сервер своими руками</title>
            <link>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8/</link>
            <pubDate>Thu, 20 Jan 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/01/dns-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B8%D0%BC%D0%B8-%D1%80%D1%83%D0%BA%D0%B0%D0%BC%D0%B8/</guid>
            <description>&lt;p&gt;Я писал коротко о &lt;a href=&#34;https://ymnuktech.ru/dns-about&#34;&gt;DNS-сервере&lt;/a&gt; и какие типы записей бывают. На самом деле это достаточно сложная система, чтобы о ней так просто говорить. Но мы же храбрые  люди и не боимся велосипедов! Попробуйем сделать DNS-сервер своими руками.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/DNS-1024x601.png&#34; alt=&#34;DNS-сервер своими руками&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;с-чего-начать&#34;&gt;С чего начать?&lt;/h2&gt;
&lt;p&gt;Для начала нужно определиться для чего он нам нужен. Лично я для себя определил, что это должен быть сервер с поддержкой Forward-запросов и «&lt;em&gt;черного списка&lt;/em&gt;» доменов. В дальнейшем я подумал а почему бы не прикрутить еще и «&lt;em&gt;белый список&lt;/em&gt;«? Но я пока не представлял себе что это будет и для чего он мне. Позже я разобрался, но об этом позже.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я писал коротко о <a href="https://ymnuktech.ru/dns-about">DNS-сервере</a> и какие типы записей бывают. На самом деле это достаточно сложная система, чтобы о ней так просто говорить. Но мы же храбрые  люди и не боимся велосипедов! Попробуйем сделать DNS-сервер своими руками.</p>
<p><img src="/images/posts/DNS-1024x601.png" alt="DNS-сервер своими руками"></p>
<h2 id="с-чего-начать">С чего начать?</h2>
<p>Для начала нужно определиться для чего он нам нужен. Лично я для себя определил, что это должен быть сервер с поддержкой Forward-запросов и «<em>черного списка</em>» доменов. В дальнейшем я подумал а почему бы не прикрутить еще и «<em>белый список</em>«? Но я пока не представлял себе что это будет и для чего он мне. Позже я разобрался, но об этом позже.</p>
<p>Второй вопрос это на чем будет все это базироваться. Самая главная сложность — это формирование запросов как клиент и ответов как сервер. В счастью есть готовые библиотеки, и не нужно будет изобретать колеса. А вот все остальное…</p>
<h2 id="структуры">Структуры</h2>
<p>Где-то и как-то нужно всю информацию хранить. Думаю, для домашнего использования, вполне должен подойти тип <strong>map</strong>. А вот и сама структура(ы):</p>
<pre tabindex="0"><code>
type DNSRecord struct {
	Name  string // Название доменного имени или его части
	Type  uint16 // Тип записи
	Class uint16 // Тип класса записи
	// Ttl   uint32 // Время жизни
	Addr  *[]Addr
	Mx    *[]Mx
	CName *[]CName
	DName *[]DName
	Ptr   *[]Ptr
	Txt   *[]Txt
	Srv   *[]Srv
}

type Addr struct {
	Addr net.IP    // Адреса
	Ttl  time.Time // ТТЛ
}

type Mx struct {
	Addr       string    // Адрес почтового сервера
	Preference uint16    // Предпочтение
	Ttl        time.Time // TTL
}

type CName struct {
	Addr string
	Ttl  time.Time
}

type DName struct {
	Addr string
	Ttl  time.Time
}

type Ptr struct {
	Addr string
	Ttl  time.Time
}

type Txt struct {
	Addr string
	Ttl  time.Time
}

type Srv struct {
	Priority uint16
	Weight   uint16
	Port     uint16
	Target   string
	Ttl      time.Time
}
</code></pre><p>Теперь есть еще другая проблема — потоконебезопасный тип <strong>map</strong>. Что-то с ним надо придумать:</p>
<pre tabindex="0"><code>
type TypeList struct {
	Mutex sync.RWMutex
	List  map[string]*DNSRecord
}
</code></pre><p>Ну и сами списки:</p>
<pre tabindex="0"><code>
var BlackList *structs.TypeList
var WhiteList *structs.TypeList
var CacheDomain *structs.TypeList
</code></pre><p>И еще немного подготовки:</p>
<pre tabindex="0"><code>
var RegexpLastComma = regexp.MustCompile(&#34;,$&#34;)
var RegexpLastDash = regexp.MustCompile(`\.$`)
var PrepareStr = regexp.MustCompile(`[\s\t]+`)
</code></pre><p>Эти регулярные выражения понадобятся для подготовки строк.</p>
<h2 id="обработчики">Обработчики</h2>
<p>Думаю создание сервера не очень интересно. Гораздо интереснее обработка запросов:</p>
<pre tabindex="0"><code>
func Handler(w dns.ResponseWriter, req *dns.Msg) {
	var err error
	var lines []dns.RR
	defer w.Close()

	m := new(dns.Msg)
	m.Authoritative = true
	m.SetReply(req)

	for _, question := range req.Question {

		var ls []dns.RR
		ls, err = Question(question.Name, question.Qtype, &amp;question)
		lines = append(lines, ls...)
		if err != nil {
			log.Println(err)
		}
	}

	m.Answer = append(m.Answer, lines...)

	err = w.WriteMsg(m)
	if err != nil {
		log.Println(err)
	}
}
</code></pre><p>Здесь мы из пакета читаем запросы по очереди и обрабатываем, подготавливая записи для ответа. Теперь сама функция <strong>Question</strong>:</p>
<pre tabindex="0"><code>
func Question(name string, qtype uint16, question *dns.Question) (lines []dns.RR, err error) {

	// Проверка в списке блокировок
	if SearchInBlackList(name, qtype) != nil {
		// Если в списке блокировки, то ничего не возвращаем, типа домена такого не существует
		return
	}

	if qtype == dns.TypeA || qtype == dns.TypeAAAA {
		if res := SearchInWhiteList(name, dns.TypeCNAME); res != nil {
			lines = append(lines, PrepareMessage(question, res)...)
			if len(*res.CName) &gt; 0 {
				for _, cname := range *res.CName {
					var ls []dns.RR
					ls, err = Question(cname.Addr, dns.TypeA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)

					ls, err = Question(cname.Addr, dns.TypeAAAA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)
				}

			}
			if len(lines) &gt; 0 {
				return
			}
		}
		if res := SearchInWhiteList(name, dns.TypeDNAME); res != nil {
			lines = append(lines, PrepareMessage(question, res)...)
			if len(*res.DName) &gt; 0 {
				for _, dname := range *res.DName {
					var ls []dns.RR
					ls, err = Question(dname.Addr, dns.TypeA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)

					ls, err = Question(dname.Addr, dns.TypeAAAA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)
				}

			}
			if len(lines) &gt; 0 {
				return
			}
		}

	}

	if res := SearchInWhiteList(name, qtype); res != nil {
		lines = append(lines, PrepareMessage(question, res)...)
		if len(lines) &gt; 0 {
			return
		}
	}

	// Проверим есть ли в кэше данные

	if qtype == dns.TypeA || qtype == dns.TypeAAAA {
		if res := SearchInCache(name, dns.TypeCNAME); res != nil {
			lines = append(lines, PrepareMessage(question, res)...)
			if len(*res.CName) &gt; 0 {
				for _, cname := range *res.CName {
					var ls []dns.RR
					ls, err = Question(cname.Addr, dns.TypeA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)

					ls, err = Question(cname.Addr, dns.TypeAAAA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)
				}

			}
			if len(lines) &gt; 0 {
				return
			}
		}

	}

	if res := SearchInCache(name, qtype); res != nil {
		lines = append(lines, PrepareMessage(question, res)...)
		if len(lines) &gt; 0 {
			return
		}
	}

	// Опросим внешний DNS для поиска записей
	msg := &amp;dns.Msg{}
	msg.SetQuestion(name, qtype)

	c := &amp;dns.Client{
		Net:          &#34;udp&#34;,
		ReadTimeout:  time.Second * 5,
		WriteTimeout: time.Second * 5,
	}

	ticker := time.NewTicker(5 * time.Second)
	defer ticker.Stop()

	ch := make(chan *dns.Msg, 1)
	var wg sync.WaitGroup

	// Функция отправления запроса на внешний DNS-сервер
	requestToDNS := func(nameserver string) {
		defer wg.Done()

		m := dns.Msg{}
		m.SetQuestion(name, qtype)

		msg, _, err := c.Exchange(&amp;m, nameserver)
		if err != nil {
			log.Printf(&#34;error: %s&#34;, err.Error())
			return
		}

		ch  0 {
				for _, cname := range *res.CName {
					var ls []dns.RR
					ls, err = Question(cname.Addr, dns.TypeA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)
					ls, err = Question(cname.Addr, dns.TypeAAAA, question)
					if err != nil {
						return
					}
					lines = append(lines, ls...)
				}

			}
			if len(lines) &gt; 0 {
				return
			}
		}

	}

	if res := SearchInCache(name, qtype); res != nil {
		lines = append(lines, PrepareMessage(question, res)...)
	}

	return
}
</code></pre><p>Функция получилась достаточно крупная и, возможно, не оптимальная. Ее надо пояснить.</p>
<p>Сначала ищем запись в черном списке. Если она найдена, то поиск останавливаем. После проверяем является ли запрос типа записи <strong>A</strong> или <strong>AAAA</strong>. Как раз это важный момент. Если запись являеися типом <strong>CNAME</strong> или <strong>DNAME</strong>, то мы не найдем запись и вернем клиенту пустой ответ, а это не правильно. После того как нашли <strong>CNAME</strong> или <strong>DNAME</strong> (если они, конечно, есть) нам снова нужно найти записи типа <strong>A</strong> или <strong>AAAA</strong>. Если мы в одном ответе не отдадим клиенту обе эти записи при наличии альясов, то у нас просто дальше работать не будет, потому что тот же браузер не делает запросы. У меня так встал <strong>FireFox</strong> и <strong>apt</strong>. Было обидно.</p>
<p>Далее после проверки записей в своем кэше, если ничего не найдено, делаем запрос в указанных серверах. Соответственно обрабатываем ответы и помещаем их в кэш. Это и будет у нас Forward-сервер. Причем, если <strong>TTL</strong> еще не закончился, то запись будет отдаваться клиенту из кэша, что уменьшит внешний запрос и, по идее, должен ускорить время ответа.</p>
<p>Собственно все.</p>
<h2 id="организация-поиска">Организация поиска</h2>
<p>Реализация поиска в списках достаточно проста:</p>
<pre tabindex="0"><code>
// Найдем домен в листе блокировки
func SearchInBlackList(domain string, domainType uint16) *structs.DNSRecord {
	return SearchInList(domain, domainType, BlackList)
}

// Найдем домен в листе допущенных
func SearchInWhiteList(domain string, domainType uint16) *structs.DNSRecord {
	return SearchInList(domain, domainType, WhiteList)
}

// Найдем домен в кэше
func SearchInCache(domain string, domainType uint16) *structs.DNSRecord {
	return SearchInList(domain, domainType, CacheDomain)
}

// Найдем домен в списке
func SearchInList(domain string, domainType uint16, list *structs.TypeList) *structs.DNSRecord {
	list.Mutex.RLock()
	defer list.Mutex.RUnlock()
	domain = RegexpLastDash.ReplaceAllString(strings.Trim(domain, &#34; &#34;), &#34;&#34;)

	if len([]rune(domain)) == 0 {
		return nil
	}

	if val, ok := list.List[domain]; ok {
		return val
	}

	if domainType == dns.TypeA || domainType == dns.TypeAAAA {
		if val, ok := list.List[fmt.Sprintf(`%s_%d`, domain, dns.TypeCNAME)]; ok {
			return val
		}
	}
	if val, ok := list.List[fmt.Sprintf(`%s_%d`, domain, domainType)]; ok {
		return val
	}
	return nil
}
</code></pre><p>Вообще ничего сложного. Просто ищем в списках не забывая накладывать блокировку, иначе программа «вылетить» из-за конкурентного доступа.</p>
<h2 id="добавление-в-кэш">Добавление в кэш</h2>
<p>Я покажу только часть кода, так как остальной год достаточно просто дописать самостоятельно. В нем присутствует немного магии, но она не сложная. И так:</p>
<pre tabindex="0"><code>
func AddInCache(msg *dns.Msg) {
	CacheDomain.Mutex.Lock()
	defer CacheDomain.Mutex.Unlock()

	if len(msg.Answer) &gt; 0 {

		for _, answer := range msg.Answer {
			AddInCacheLine(answer)
		}
	}

	if len(msg.Extra) &gt; 0 {
		for _, extra := range msg.Extra {
			AddInCacheLine(extra)
		}
	}
}

func AddInCacheLine(answer dns.RR) {
	var msgi interface{} = answer
	switch v := (msgi).(type) {
	case *dns.A:
		domain := RegexpLastDash.ReplaceAllString(strings.Trim(v.Hdr.Name, &#34; &#34;), &#34;&#34;)
		if dnsRecord := CacheDomain.List[fmt.Sprintf(`%s_%d`, domain, v.Hdr.Rrtype)]; dnsRecord != nil {
			// Добавим адрес в существующую запись
			// Найдем есть ли этот адрес уже в записи
			if dnsRecord.Addr != nil {
				if len(*dnsRecord.Addr) == 0 {
					var a structs.Addr
					a.Addr = v.A
					a.Ttl = time.Now().Local().Add(time.Second * time.Duration(v.Hdr.Ttl))
					dnsRecord.Addr = &amp;[]structs.Addr{a}
				} else {
					search := false
					for i := range *dnsRecord.Addr {
						if net.IP.Equal((*dnsRecord.Addr)[i].Addr, v.A) {
							search = true
							(*dnsRecord.Addr)[i].Ttl = time.Now().Local().Add(time.Second * time.Duration(v.Hdr.Ttl))
						}
					}
					if !search {
						var a structs.Addr
						a.Addr = v.A
						a.Ttl = time.Now().Local().Add(time.Second * time.Duration(v.Hdr.Ttl))
						addrs := *dnsRecord.Addr
						addrs = append(addrs, a)
						dnsRecord.Addr = &amp;addrs
					}
				}
			} else {
				var a structs.Addr
				a.Addr = v.A
				a.Ttl = time.Now().Local().Add(time.Second * time.Duration(v.Hdr.Ttl))
				dnsRecord.Addr = &amp;[]structs.Addr{a}
			}
		} else {
			dnsRecord := structs.DNSRecord{
				Name:  domain,
				Type:  v.Hdr.Rrtype,
				Class: v.Hdr.Class,
				Addr: &amp;[]structs.Addr{
					{
						Addr: v.A,
						Ttl:  time.Now().Local().Add(time.Second * time.Duration(v.Hdr.Ttl)),
					},
				},
			}
			CacheDomain.List[fmt.Sprintf(`%s_%d`, dnsRecord.Name, dnsRecord.Type)] = &amp;dnsRecord
		}
        }
}
</code></pre><p>Собственно нужно реализовать конструкцию <strong>switch</strong> для разных типов записей. Кто знает более элегантный способ, пожалуйста, не стесняйтесь.</p>
<h2 id="ответы">Ответы</h2>
<p>Формирование ответов тоже достаточно просто получается. В функции нужно все так же дописать конструкцию <strong>switch</strong>. Главное сам принцип:</p>
<pre tabindex="0"><code>
func PrepareMessage(req *dns.Question, res *structs.DNSRecord) (lines []dns.RR) {
	// Нашли в кэше. Сразу отдадим клиенту
	head := dns.RR_Header{
		Name:   req.Name,
		Rrtype: req.Qtype,
		Class:  req.Qclass,
	}
	now := time.Now().Local()
	switch res.Type {
	case dns.TypeA:
		for _, addr := range *res.Addr {
			if addr.Ttl.After(now) {

				line := &amp;dns.A{
					Hdr: head,
					A:   addr.Addr,
				}
				line.Hdr.Ttl = uint32(addr.Ttl.Sub(now) / time.Second)
				line.Hdr.Rrtype = dns.TypeA
				line.Hdr.Name = fmt.Sprintf(&#34;%s.&#34;, res.Name)
				lines = append(lines, line)
			}
		}
	}

	return
}
</code></pre><p>И это все.</p>
<h2 id="списочки">Списочки</h2>
<p>Эта вся работа учитывает и <em>черный список</em> и <em>белый список</em>. Если с черным списком все понятно, то вот с белым хотелось бы определиться. В итоге я решил, что это будут записи как в самом обычном DNS-сервере.</p>
<p>Как это быдет работать? Все просто! Если мы добавим нужную запись нужного типа в белый список, то мы его будем хранить все время. Получится такой мини-dns для домашних собственных нужд. Чтобы такое реализовать нужно подумать над форматами.</p>
<p>Для <em>черного списка</em> все элементарно: просто одна строка — один домен. Для белого списка будет определенный формат:</p>
<pre tabindex="0"><code>
  [приоритет целевого хоста] [вес записи] [порт]
</code></pre><p>где:</p>
<p>Домен — доменное имя
Тип записи — A, AAAA, CNAME, DNAME, MX, SRV
Приоритет целевого хоста — приоритет хоста для записи SRV. Более низкое значение имеет более высокий приоритет
Вес записи — относится к типу записи MX и SRV
Порт — порт TCP или UDP, на котором работает сервис
Адрес — конечный адрес</p>
<p>Все TTL устанавливаются по умолчанию 600.</p>
<p>Списки необходимо загрузить. Черный список:</p>
<pre tabindex="0"><code>
func LoadBlacklist() {
	if _, err := os.Stat(&#34;database/blacklist.txt&#34;); errors.Is(err, os.ErrNotExist) {
		return
	}

	file, err := os.Open(&#34;database/blacklist.txt&#34;)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)

	for scanner.Scan() {
		str := strings.Trim(scanner.Text(), &#34; &#34;)
		if len([]rune(str)) &gt; 0 {
			if ([]rune(str))[0] != &#39;#&#39; {
				domain := strings.ToLower(strings.Trim(str, &#34; &#34;))
				BlackList.List[domain] = &amp;structs.DNSRecord{}
			}
		}
	}
}
</code></pre><p>Вообще ничего сложного! Просто читаем построчно и добавляем в список.</p>
<p>Теперь белый список:</p>
<pre tabindex="0"><code>
func LoadWhitelist() {
	if _, err := os.Stat(&#34;database/whitelist.txt&#34;); errors.Is(err, os.ErrNotExist) {
		return
	}

	file, err := os.Open(&#34;database/whitelist.txt&#34;)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)

	for scanner.Scan() {
		strs := strings.Split(PrepareStr.ReplaceAllString(strings.Trim(scanner.Text(), &#34; &#34;), &#34; &#34;), &#34; &#34;)
		if len(strs) &gt;= 3 {
			strs[0] = strings.ToLower(strs[0])
			strs[1] = strings.ToUpper(strs[1])
			strs[2] = strings.ToLower(strs[2])

			if len(strs) == 4 {
				strs[3] = strings.ToLower(strs[3])
			}

			switch strs[1] {
			case &#34;A&#34;:
				dnsRecord := structs.DNSRecord{
					Name: strs[0],
					Type: dns.TypeA,
					Addr: &amp;[]structs.Addr{
						{
							Addr: net.ParseIP(strs[2]),
							Ttl:  time.Now().Local().Add(time.Second * time.Duration(600)),
						},
					},
				}
				WhiteList.List[fmt.Sprintf(`%s_%d`, strs[0], dns.TypeA)] = &amp;dnsRecord
			default:
				err := fmt.Errorf(`unknow type record: %s`, strs[1])
				panic(err)
			}
		}
	}
}
</code></pre><p>Немного сложнее, но не на много. Здесь так же нужно дописать <strong>switch</strong> для требуемых типов записей.</p>
<h2 id="сервер-готов">Сервер готов.</h2>
<p>Да, это простейший сервер, который не учитывает очень многое. Во всяком случае я его писал для своего домашнего сервера и он у меня работает.</p>
<p>Не спорю, в нем нет интерфейса, хотелось бы прикрутить REST API для работы, да еще и перезагружать нужно, чтобы перечитать списки. Минус еще в том, что при перезагрузке теряется кэш. Да, его хранить нет смысла, но при перезапуске клиенты перестают работать пока сервер снова не включится. В общем есть над чем работать, а пока полный исходный код находится в <a href="https://gitlab.com/ymnukus/dns-adblock">git-е</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Что такое DNS и с чем его едят</title>
            <link>https://ymnuktech.ru/posts/2022/01/%D1%87%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-dns-%D0%B8-%D1%81-%D1%87%D0%B5%D0%BC-%D0%B5%D0%B3%D0%BE-%D0%B5%D0%B4%D1%8F%D1%82/</link>
            <pubDate>Thu, 13 Jan 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/01/%D1%87%D1%82%D0%BE-%D1%82%D0%B0%D0%BA%D0%BE%D0%B5-dns-%D0%B8-%D1%81-%D1%87%D0%B5%D0%BC-%D0%B5%D0%B3%D0%BE-%D0%B5%D0%B4%D1%8F%D1%82/</guid>
            <description>&lt;p&gt;Сегодня опробуем разобраться что такое DNS и с чем его едят (используют). На эту тему уже написано и рассказано овер-дофига раз. Еще одну можно было бы и не писать. А так хочется. Ну а раз хочется, то, наверно, можно.&lt;/p&gt;
&lt;h2 id=&#34;что-за-зверь&#34;&gt;Что за зверь?&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://ru.wikipedia.org/wiki/DNS&#34;&gt;DNS&lt;/a&gt; или Domain Name System — это специализированная система которая умеет по имени домена определять какие-то параметры работы в сети Internet. Обычно его используют для получения IP-адреса по имени. Например, когда вы вводите адрес в браузере, то перед подключением к серверу он сначала спрашивает у службы DNS какой реальный IP-адрес у сервера и, если все прошло успешно, подключается непосредственно к серверу и запрашивает страницы. Это было придумано для того, чтобы не запоминать цифровые адреса, а какие-то осмысленные имена.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Сегодня опробуем разобраться что такое DNS и с чем его едят (используют). На эту тему уже написано и рассказано овер-дофига раз. Еще одну можно было бы и не писать. А так хочется. Ну а раз хочется, то, наверно, можно.</p>
<h2 id="что-за-зверь">Что за зверь?</h2>
<p><a href="https://ru.wikipedia.org/wiki/DNS">DNS</a> или Domain Name System — это специализированная система которая умеет по имени домена определять какие-то параметры работы в сети Internet. Обычно его используют для получения IP-адреса по имени. Например, когда вы вводите адрес в браузере, то перед подключением к серверу он сначала спрашивает у службы DNS какой реальный IP-адрес у сервера и, если все прошло успешно, подключается непосредственно к серверу и запрашивает страницы. Это было придумано для того, чтобы не запоминать цифровые адреса, а какие-то осмысленные имена.</p>
<p>Помимо такой информации DNS может хранить и некоторую другую информацию о домене. Достаточно часто ею пользуются почтовые серверы и клиенты, корпоративные сети, да и просто разработчики и администраторы для настройки каких-то систем и программ и т.д. По сути это неотъемлемая часть нашей сегодняшней повседневной жизни.</p>
<h2 id="как-хранится-информация">Как хранится информация?</h2>
<p>Информацию в DNS можно представить в виде специализированной базы, в которой хранятся записи с определенными их типами. Каждый тип записи несет собственную нагрузку и отвечает за свою область деятельности.</p>
<h2 id="какие-записи-бывают">Какие записи бывают?</h2>
<p>Типов записей достаточно <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B8%D0%BF%D1%8B_%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%BD%D1%8B%D1%85_%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B5%D0%B9_DNS">много</a>. Каждая из них отвечает за свою область деятельности. Все их рассматривать я не буду, а только расскажу о тех, с которыми я лично столкнулся.</p>
<p>Важно знать, что существует 2 основных типа адресации в Internet: <a href="https://ru.wikipedia.org/wiki/IPv4">IPv4</a> и <a href="https://ru.wikipedia.org/wiki/IPv6">IPv6</a>. На самом деле их гораздо больше, нам на текущий момент они не нужны.</p>
<p>Чтобы посмотреть самостоятельно эти записи нужно установить соответствующее по. В Debian:</p>
<pre tabindex="0"><code>
sudo apt install dnsutils
</code></pre><p>Чтобы установить утилиту <strong>dig</strong> в <strong>Windows</strong> нужно <a href="https://www.isc.org/download/">загрузить</a> и установить соответствующий скомпилированный пакет <a href="https://ru.wikipedia.org/wiki/BIND">BIND9</a>.</p>
<h3 id="a">A</h3>
<p>Этот тип записи отвечает за тип ресурса IPv4. Например, роутер в домашней сети имеет адрес 192.168.0.1. Чтобы к нему подключиться и настроить обычно указывается этот адрес в браузере. С помощью DNS можно было бы задать ему доменный адрес, например, router.loc. Тогда DNS запись будет выглядеть так:</p>
<pre tabindex="0"><code>
router.loc.             72220   IN      A   192.168.0.1
</code></pre><p>Точка в конце имени домена говорит о том, что просмотр начинается от корня доменной системы. Об этом написано в <a href="https://datatracker.ietf.org/doc/html/rfc1035">спецификациях</a>.</p>
<p>Чтобы просмотреть информацию нужно выполнить команду:</p>
<pre tabindex="0"><code>
dig
</code></pre><h3 id="аааа">АААА</h3>
<p>Этот тип записи является эквивалентным типу записи <strong>A</strong>. Разница в том, что возвращается не IPv4, а IPv6.</p>
<pre tabindex="0"><code>
dig  A
</code></pre><h3 id="mx">MX</h3>
<p>Этот тип записи нужен, чтобы определить по основному домену какой домен используется для сервера электронной почты. <strong>MX</strong> расшифровывается как <strong>Mail Exchange</strong>.</p>
<p>Мы все привыкли отправлять электронную почту в виде <em><a href="mailto:user@server1.loc">user@server1.loc</a></em>, где user — имя пользователя а server1.loc — сервер, на котором пользователь зарегистрирован. Теперь представьте, что нужно отправить письмо пользователю <a href="mailto:user@server2.loc">user@server2.loc</a> от пользователя <a href="mailto:user@server1.loc">user@server1.loc</a>. server1.loc и server2.loc — это абсолютно 2 разных сервера. Так как же серверы понимают куда отправлять письмо?</p>
<p>Все просто! Почтовый сервер server1.loc смотрит на адрес, где после символа <strong>@</strong> указан почтовый сервер. Он спрашивает у DNS какой домен используется для этого сервера:</p>
<pre tabindex="0"><code>
dig  MX
</code></pre><p>Если такая запись есть, то DNS возвращает домен этого сервера. На самом деле обычный домен, например сайта, и домен  почтового обменника могут различаться как логически, так и физически.</p>
<p>После этого, как получен домен почтового обменника, сервер запрашивает адрес из записи <strong>A</strong> или <strong>AAAA</strong> (смотря какая адресация требуется) и уже может спокойно подключиться для обмена корреспонденцией.</p>
<h3 id="ptr">PTR</h3>
<p>Еще одна запись, которая часто используется с почтовыми серверами. Ее цель сделать обратное преобразование, т.е. какое доменное имя у адреса.</p>
<pre tabindex="0"><code>
dig -x
</code></pre><p>Частая практика это проверка почтовым сервером отправитель, т.е. принимающий почтовый сервер спрашивает доменное имя. Если такой записи нет или она не ведет на тот же сервер, то должен возникнуть вопрос а стоит ли доверять ему. Это делается для защиты от «<a href="https://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B0%D0%BC">спамеров</a>«.</p>
<h3 id="cname">CNAME</h3>
<p>Эта запись используется для определения псевдонимов домена. К примеру, изменился домен сайта, но люди еще по старой памяти открывают старое название. Чтобы они так же могли продолжать работать с помощью <strong>CNAME</strong> устанавливается перенаправление на другой домен, а потом уже для другого домена определяется его реальный адрес. Обычно <strong>CNAME</strong> используется временно и удаляется через какой-то определенный срок.</p>
<pre tabindex="0"><code>
dig  CNAME
</code></pre><h3 id="dname">DNAME</h3>
<p>Эта запись так же используется для определения псевдонимов домена, но на постоянной основе. Принцип работы его такой же.</p>
<pre tabindex="0"><code>
dig  DNAME
</code></pre><h3 id="txt">TXT</h3>
<p>Данная запись является информационной. В нее добавляется различный текст, а он в свою очередь может являться определенными настройками. Так в записях <strong>TXT</strong> могут храниться записи <strong>SPF</strong>, в которых указывается какая-то информация для почтового сервера.</p>
<pre tabindex="0"><code>
dig  TXT
</code></pre><h3 id="srv">SRV</h3>
<p>Эта запись предназначена для определения какая служба где находится. Так эту запись используют для определения серверов <strong>SIP</strong> (телефония), <strong>Active Directory</strong> (в корпоративных сетях), <strong>XMPP</strong> (служба обмена мгновенных сообщений) и т.д. У нее все та же роль: узнать реальный сервер, с которым можно установить соединение для дальнейшей работы.</p>
<pre tabindex="0"><code>
dig _-._. SRV
</code></pre><p>В ответ DNS-сервер отвечает с именем домена, где расположен сервер, приоритет, вес и портом, к которому необходимо подключаться.</p>
<h2 id="это-все">Это все?</h2>
<p>На самом деле существует очень много типов записей, с которыми работает DNS. Я рассмотрел лишь популярные, которые используются на стороне клиента. Существуют так же записи, например, <strong>SOA</strong> с эталонной информацией о домене, **NS **с информацией о сервере, являющийся владельцем домена и т.д. Все эти записи описаны в специальных документах под названием <a href="https://ru.wikipedia.org/wiki/RFC">RFC</a> и имеющих свой номер с описанием спецификации. Для тех, кто хочет использовать его на <a href="https://ymnuktech.ru/tag/home-server/">домашнем сервере</a> можно установить специализированные программы, которые будут являться как самостоятельным сервером, так и, так называемым, Forward-сервером.</p>
<p>Forward-сервер — это такой сервер, который является промежуточным и способен «кэшировать» запросы, чтобы разгрузить основной сервер и минимизировать время ответа клиенту.</p>
]]></content>
        </item>
        
        <item>
            <title>Pi-Hole (домашний сервер)</title>
            <link>https://ymnuktech.ru/posts/2022/01/pi-hole-%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</link>
            <pubDate>Mon, 10 Jan 2022 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2022/01/pi-hole-%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</guid>
            <description>&lt;p&gt;Порой хочется сделать фильтрацию нежелательного контента. Уже многие знают про плагины в браузеры, типа AdBlock и их разновидности. Так же есть еще целая куча программ, которая ставится на компьютер и что-то там фильтрует. А еще есть антивирусы, которые имеют ту же функцию (зачастую в платном варианте).&lt;/p&gt;
&lt;p&gt;Бродя по просторам интернета наткнулся я на &lt;a href=&#34;https://vc.ru/tech/258921-inzhener-sobral-odnoplatnyy-kompyuter-dlya-antispama-vnutri-banki-ot-myasnyh-konservov-spam&#34;&gt;статью&lt;/a&gt; с проектом &lt;a href=&#34;https://pi-hole.net/&#34;&gt;Pi-Hole&lt;/a&gt;. Крутость этой штуки в том, что она ставится на одноплатник и просто работает. Ну у нас же есть уже готовый &lt;a href=&#34;https://ymnuktech.ru/home_server_hardware/&#34;&gt;домашний сервер&lt;/a&gt;! Тогда приступим.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Порой хочется сделать фильтрацию нежелательного контента. Уже многие знают про плагины в браузеры, типа AdBlock и их разновидности. Так же есть еще целая куча программ, которая ставится на компьютер и что-то там фильтрует. А еще есть антивирусы, которые имеют ту же функцию (зачастую в платном варианте).</p>
<p>Бродя по просторам интернета наткнулся я на <a href="https://vc.ru/tech/258921-inzhener-sobral-odnoplatnyy-kompyuter-dlya-antispama-vnutri-banki-ot-myasnyh-konservov-spam">статью</a> с проектом <a href="https://pi-hole.net/">Pi-Hole</a>. Крутость этой штуки в том, что она ставится на одноплатник и просто работает. Ну у нас же есть уже готовый <a href="https://ymnuktech.ru/home_server_hardware/">домашний сервер</a>! Тогда приступим.</p>
<p><img src="/images/posts/fire-off-dns.png" alt="Pi-Hole (домашний сервер)"></p>
<h2 id="немного-теории">Немного теории</h2>
<p>Основной принцип работы данного сервера, это обычный DNS-сервер. Через него проходят DNS-запросы и проверяются нет ли записи в черном списке. Если же нету, то выполняется forward-запрос на внешний DNS-сервер. Таким образом мы просто фильтруем домены. Все достаточно просто.</p>
<h2 id="практика">Практика</h2>
<p>И так, как обычно по канонам мастерства нашей настройки создадим файл /opt/pihole/docker-compose.yml следующего содержимого:</p>
<pre tabindex="0"><code>
version: &#34;2.4&#34;

services:
  pihole:
    image: pihole/pihole:2021.11
    restart: unless-stopped
    volumes:
      - type: bind
        source: /mnt/nfs/pihole/etc-pihole/
        target: /etc/pihole/
      - type: bind
        source: /mnt/nfs/pihole/etc-dnsmasq.d/
        target: /etc/dnsmasq.d/
    mem_limit: 256m
    mem_reservation: 64m
    ports:
      - &#34;53:53/tcp&#34;
      - &#34;53:53/udp&#34;
      - &#34;8086:80/tcp&#34;
    environment:
      TZ: &#39;Europe/Moscow&#39;
      WEBPASSWORD:
    cap_add:
      - NET_ADMIN
</code></pre><p>После создаем файл <strong>/etc/systemd/system/pihole.service</strong>:</p>
<pre tabindex="0"><code>
[Unit]
Description=PiHole docker-compose
Requires=docker.service
After=docker.service

[Service]
Restart=always

WorkingDirectory=/opt/pihole/

# Compose up
ExecStart=/usr/bin/docker-compose -f docker-compose.yml up

# Compose down, remove containers
ExecStop=/usr/bin/docker-compose -f docker-compose.yml down

[Install]
WantedBy=multi-user.target
</code></pre><p>Далее создаем пару директорий:</p>
<pre tabindex="0"><code>
mkdir -p /mnt/nfs/pihole/etc-pihole
mkdir -p /mnt/nfs/pihole/etc-dnsmasq.d
</code></pre><p>И все это дело запускаем:</p>
<pre tabindex="0"><code>
systemctl daemon-reload
systemctl enable pihole &amp;&amp; systemctl enable pihole
</code></pre><p>После заходим по адресу http://:8086 и смотрим, что все работает.</p>
<h2 id="настройка-домашней-сети">Настройка домашней сети</h2>
<p>Теперь требуется немного настроить домашнюю сеть. Так как дома обычно используется роутер для подключения к интернету, то он является шлюзом. Обычно при включении компьютера роутер выдает адрес и некоторые сетевые настройки. Нас интересует настройка DNS. Для этого идем в роутер и в настройках DHCP-сервера указываем IP-адрес нашего нового DNS-сервера.</p>
<p><img src="/images/posts/router.png" alt=""></p>
<p>Если же на устройствах настроена «статика», то настраиваем уже устройства.</p>
<h2 id="решаем-проблемы">Решаем проблемы</h2>
<p>После того, как зашли в web-интерфейс попробуем добавить домен в whitelist или blacklist. Если вываливается ошибка, то придется кое-что сделать вручную. Для этого в консоли выполняем:</p>
<pre tabindex="0"><code>
docker exec -it pihole_pihole_1 chown -R www-data:pihole /etc/pihole
docker exec -it pihole_pihole_1 chmod -R 744 /etc/pihole
</code></pre><p>В данном случае мы выставляем права доступа для файлов. Так у меня случае они устанавливались не корректно (а может и корректно). В любом случае мне помогло.</p>
<h2 id="проверка">Проверка</h2>
<p>Чтобы проверить как это все работает идем по адресу <strong><a href="https://d3ward.github.io/toolz/adblock.html">https://d3ward.github.io/toolz/adblock.html</a></strong> и проверяем результат. Если все устраивает, то оставляем как есть. Если же нет, то добавляем то что нужно.</p>
<p><img src="/images/posts/test-ads-1024x860.png" alt=""></p>
<h2 id="что-получилось">Что получилось</h2>
<p>Теперь у нас в локальной сети есть свой DNS-сервер, который является еще и кэширующим, а на его основе можно отсечь нежелательные домены. Можно вписать домены, которые раздражают (реклама) и/или нежелательные ресурсы для детей (да и для взрослых). Уже прогресс.</p>
]]></content>
        </item>
        
        <item>
            <title>Определение адресов клиентов в PHP</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%BE%D0%B2-%D0%BA%D0%BB%D0%B8%D0%B5%D0%BD%D1%82%D0%BE%D0%B2-%D0%B2-php/</link>
            <pubDate>Thu, 30 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D0%BE%D0%BF%D1%80%D0%B5%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D0%B0%D0%B4%D1%80%D0%B5%D1%81%D0%BE%D0%B2-%D0%BA%D0%BB%D0%B8%D0%B5%D0%BD%D1%82%D0%BE%D0%B2-%D0%B2-php/</guid>
            <description>&lt;p&gt;Это скорее краткая заметка чем статья, но все же стоит это учитывать. В &lt;strong&gt;PHP&lt;/strong&gt;, как у в любой серверной обработке, есть заголовки, которые приходят и их можно использовать для обработки запросов, например, для логирования действий пользователей. Обычно многие привыкли, что поставил &lt;a href=&#34;https://ymnuktech.ru/home-server-gateway-internet&#34;&gt;сервер&lt;/a&gt; и он работает. К сожалению, в частности в &lt;em&gt;WordPress&lt;/em&gt;, я столкнулся с тем, что учитывается адрес моего &lt;em&gt;Reverse-прокси&lt;/em&gt;, а не реальный адрес, с которого подключился посетитель. Вот с этим и будем разбираться.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Это скорее краткая заметка чем статья, но все же стоит это учитывать. В <strong>PHP</strong>, как у в любой серверной обработке, есть заголовки, которые приходят и их можно использовать для обработки запросов, например, для логирования действий пользователей. Обычно многие привыкли, что поставил <a href="https://ymnuktech.ru/home-server-gateway-internet">сервер</a> и он работает. К сожалению, в частности в <em>WordPress</em>, я столкнулся с тем, что учитывается адрес моего <em>Reverse-прокси</em>, а не реальный адрес, с которого подключился посетитель. Вот с этим и будем разбираться.</p>
<h2 id="что-такое-http-заголово">Что такое HTTP-заголово</h2>
<p>В протоколе HTTP при запросе страницы передаются какие-то стандартные заголовки, но так же могут передаваться и дополнительные. Это можно посмотреть в браузере, если нажать <strong>F12</strong>.</p>
<p><img src="/images/posts/browser_headers-1024x275.png" alt="Определение адресов клиентов в PHP"></p>
<p>Ко всему прочему интерпретатор <strong>PHP</strong> предоставляет еще кое-какую дополнительную информацию, например, адрес сервера, адрес клиента, строки запроса и т.д. Для этого у него есть специальный объект <strong>$_SERVER</strong>. Всю информацию можно посмотреть в <a href="https://www.php.net/manual/ru/reserved.variables.server.php">документации</a>. У меня Nginx настроен как Reverse-прокси, т.е. <em>web-сервер</em> получает запрос, смотрит что пришло в заголовках и на основе этого определяет куда этот запрос направить дальше. В моем случае перед передачей запроса дальше web-сервер добавляет следующие заголовки:</p>
<pre tabindex="0"><code>
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
</code></pre><p>Соответственно на сервер передаются те заголовки, которые являются, так сказать, стандартными + дополнительные.</p>
<h2 id="ну-и-в-чем-проблема">Ну и в чем проблема?</h2>
<p>При работе серверного кода разработчик уже с этими данными что-то делает. В моем случае в <strong>$_SERVER[‘REMOTE_ADDR‘]</strong> содержится адрес моего сервера, а адрес клиента находится в <strong>$_SERVER[‘X-FORWARDED-FOR’]</strong>. По хорошему WordPress должен его обрабатывать, но он его в упор не видит. Почему этот заголовок? Ну тут я не могу ответить. Сколько я смотрел в интернете, практически везде задают именно имя.</p>
<h2 id="и-что-делать">И что делать?</h2>
<p>Для начала нужно понять, что PHP, по сути каждый раз выполняет какой-то <em>скрипт</em> и при каждом выполнении загружает файл настроек (например). Тогда, самым простым вариантом можно модифицировать его немного так, чтобы в <strong>PHP</strong> у нас <strong>X-FORWARDED-FOR</strong> как-то присваивался в <strong>REMOTE_ADDR</strong>. Я бы не стал трогать файлы самого движка, так как обновления могут все стереть и придется это делать каждый раз, а вот файл настроек практически не меняется, если только нет очень крупных изменений. Логично…</p>
<p>В таком случае заходим в файл wp-config.ph и опускаемся где у нас присутствует строчка проверки условия с параметром <strong>HTTP_X_FORWARDED_PROTO.</strong> После этого условия добавляем свое:</p>
<pre tabindex="0"><code>
if (isset($_SERVER[&#39;HTTP_X_FORWARDED_FOR&#39;])) {
        $_SERVER[&#39;REMOTE_ADDR&#39;] = $_SERVER[&#39;HTTP_X_FORWARDED_FOR&#39;];
}
</code></pre><p>На мой взгляд вполне подойдет.</p>
<p>Теперь, если у нас появляется заголовок <strong>HTTP_X_FORWARDED_FOR</strong>, то мы его присваиваем параметру <strong>REMOTE_ADDR</strong> и <em>WordPress</em> уже будет фиксировать нормально адреса.</p>
<h2 id="итог">Итог</h2>
<p>Если есть в WordPress другое решение без модификации кода, то, наверно, это будет лучше. Я лично не нашел. По хорошему нужно не модифицировать заголовки, а обрабатывать их. В данном случае, на мой взгляд, это такой небольшой <em>хак</em>.</p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер (шлюз в интернете)</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%88%D0%BB%D1%8E%D0%B7-%D0%B2-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%BD%D0%B5%D1%82%D0%B5/</link>
            <pubDate>Mon, 27 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%88%D0%BB%D1%8E%D0%B7-%D0%B2-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%BD%D0%B5%D1%82%D0%B5/</guid>
            <description>&lt;p&gt;Я уже писал про &lt;a href=&#34;https://ymnuktech.ru/home_server_hardware/&#34;&gt;домашний сервер&lt;/a&gt; &lt;a href=&#34;https://ymnuktech.ru/home-server-self-cloud/&#34;&gt;не один раз&lt;/a&gt; и эта статья должна стать завершающей в данном цикле. В этот раз мне требуется организовать доступ к своему «&lt;strong&gt;домашнему железу&lt;/strong&gt;«, чтобы можно было работать из любой точки города, региона или страны. Есть 2 варианта организации такого доступа: купить внешний IP-адрес у провайдера на &lt;strong&gt;постоянку&lt;/strong&gt; (белый IP) или арендовать внешний VPS, который будет являться шлюзом в интернете. Лично я для себя решил выбрать VPS. Ну что же… Release it!&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про <a href="https://ymnuktech.ru/home_server_hardware/">домашний сервер</a> <a href="https://ymnuktech.ru/home-server-self-cloud/">не один раз</a> и эта статья должна стать завершающей в данном цикле. В этот раз мне требуется организовать доступ к своему «<strong>домашнему железу</strong>«, чтобы можно было работать из любой точки города, региона или страны. Есть 2 варианта организации такого доступа: купить внешний IP-адрес у провайдера на <strong>постоянку</strong> (белый IP) или арендовать внешний VPS, который будет являться шлюзом в интернете. Лично я для себя решил выбрать VPS. Ну что же… Release it!</p>
<p><img src="/images/posts/photo_2021-12-10_22-15-29-225x300.jpg" alt="Домашний сервер (шлюз в интернете)"></p>
<h2 id="выбор-vps">Выбор VPS</h2>
<p>Тут я мог бы провести анализ рынка облаков, стоимости, качества, стабильности. Но я этого делать не буду. На рынке услуг просто масса и можно выбирать все что угодно согласно Вашему бюджету, предпочтению, желанию и требованиям. Вторая причина по которой я этого делать не буду — это реклама. Ну вот не хочу и все!</p>
<p>Чтобы подобрать для себя VPS нужно определиться с техническими характеристиками. В моем случае у меня будет установлено всего 3 основных приложения: openvpn, nginx и certbot. Для чего они нужны я буду говорить далее. На весь этот ворох приложений на текущий момент у меня уходит 260МБ ОЗУ, загрузка CPU (на одно ядро) не более 30% и HDD занято 5.5ГБ. Так что выбираем под себя (или даже один из самых дешевых на сколько это возможно). На счет скорости сети смотрим и оцениваем сами. Может вы будете гонять терабайты данных на высоких скоростях, а может и 100 мегабайт превышать не будет. Так что прикидываем и определяемся.</p>
<p>Что на счет операционной системы, то выбираем то, что умеем настраивать лучше всего. Просто потому что так проще. Лично я для себя выбрал CentOS из тех дистрибутивов, что были доступны.</p>
<h2 id="настройка-хостера">Настройка хостера</h2>
<p>Для начала нужно немного подготовить настройки хостинга. Первым делом идем в настройки самого хостера и настраиваем сеть. Первое это нужно убедиться, что нам дали внешний IP, по которому мы будем подключаться. После заходим в настройки безопасности сети и проверяем какие порты открыты. Понадобятся следующие порты:</p>
<ul>
<li>80 (http)- 443 (https)- 22 (ssh)- 1194 (openvpn)</li>
</ul>
<p>Все остальные можно закрыть (они не понадобятся). Как это сделать я не буду рассказывать, так как у каждого хостера свой интерфейс с настройками. По факту ничего сложного нету. Что касается портов, то 22-й множно перенести на другой, например 2222 с перенаправлением порта на 22 самой виртуальной машины (VM), а 1194 можно просто использовать другой, например, 11194й, ну или какой-то еще по желанию. 80й и 443й должны остаться такими, чтобы web-браузеры могли спокойной работать.</p>
<p>Если же хостинг не предоставляет таких настроек и Вам открыты все порты, то хорошим тоном будет настройка межсетевого экрана на самой VPS, иначе получится проходной двор. В общем «<strong>не секурно</strong>» как-то…</p>
<h2 id="подготовка-ос">Подготовка ОС</h2>
<p>Чтобы не мешал SELinux — выключим его. Для этого в консоли переходим в <strong>root</strong> и выполняем:</p>
<pre tabindex="0"><code>
setenforce 0
</code></pre><p>И в файле <strong>/etc/sysconfig/selinux</strong> устанавливаем параметр <strong>SELINUX=disabled</strong>. Кстати, в CentOS по умолчанию редактор <strong>vim</strong>, но те кто хочет <strong>nano</strong> ставим так:</p>
<pre tabindex="0"><code>
dnf install nano
</code></pre><p>Теперь подключаем <strong>epel</strong> (а вдруг пригодится):</p>
<pre tabindex="0"><code>
yum install epel-release
</code></pre><p>Далее устанавливаем nginx по <a href="https://nginx.org/ru/linux_packages.html#RHEL-CentOS">мануалу</a>. Я не буду описывать, потому что все предельно просто.</p>
<p>Теперь устанавливаем некоторые пакеты:</p>
<pre tabindex="0"><code>
dnf install openvpn certbot openvpn easy-rsa
</code></pre><p>Здесь мы устанавливаем CertBot для Let’s Encrypt, OpenVPN для соединения с нашим домашним сервером и easy-rsa для генерирования сертификатов и настройки тоннеля.</p>
<h2 id="openvpn">OpenVPN</h2>
<p>Не хочу расписывать целые талмуты настройки тоннеля, так как этого материала <a href="https://www.google.com/search?client=firefox-b-e&amp;q=CentOS&#43;0&#43;OpenVPN">полно</a>. Все же есть некоторый моменты на которые я наткнулся и о них я немного расскажу.</p>
<p>Первым делом действуем по инструкции и генерируем сертификаты. Здесь все выполняется пошагово. Различия начинаются (как у меня сделано) в конфигурационных файлах самого <strong>OpenVPN</strong>.</p>
<p>На сервере файлы распологаются по пути <strong>/etc/openvpn/server</strong> и в моем случае конфигурационный файл называется <strong>gateway.conf</strong>:</p>
<pre tabindex="0"><code>
port 1194
proto tcp
dev tun

server 10.26.0.0 255.255.255.0

keepalive 10 120

dh dh.pem
ca ca.crt
cert gateway.crt
key gateway.key
tls-auth ta.key 0

verb 3

status /var/log/openvpn/openvpn-status.log 1
status-version 3
log-append /var/log/openvpn/openvpn-client.log

client-config-dir /etc/openvpn/clients

comp-lzo
</code></pre><p>Вот тут появляются моменты, с которыми я столкнулся.</p>
<h3 id="server-102600-2552552550">server 10.26.0.0 255.255.255.0</h3>
<p>Здесь я устанавливаю водсеть самого vpn. У меня это будет <em>10.26.0.0/24</em>. Соответственно выставляйте свою, которая Вам нравится. Просто это класс корпоративной сети. За подробностями стоит обратиться к соответствующей технической литературе.</p>
<h3 id="keepalive-10-120">keepalive 10 120</h3>
<p>Вот этот параметр говорит, что каждые 10 секунд и, если в течении 120 секунд не будет ответа, то попытаться перезапустить туннель. Без этого у меня туннель зависал и отваливался. Обидно даже.</p>
<h3 id="comp-lzo">comp-lzo</h3>
<p>Тут все просто! Включаем сжатие трафика в туннеле алгоритмом <strong>lzo</strong>. Бинарные сжатые данные конечно же не сожмутся (может даже чуть увеличится объем), а вот на текстовых данные это будет видно. Порой спорный параметр, нужно смотреть, но в общем и целом, думаю, использовать можно.</p>
<h3 id="client-config-dir-etcopenvpnclients">client-config-dir /etc/openvpn/clients</h3>
<p>В этой директории будут лежать некоторые настройки для клиентов. Здесь все просто. У меня лежит один файл <strong>apps</strong> с таким содержимым:</p>
<pre tabindex="0"><code>
ifconfig-push 10.26.0.253 10.26.0.1
</code></pre><p>Тут говорится, что подключенномe клиенту <strong>apps</strong> (так называется сертификат клиента и так его показывает openvpn) присвоить адрес <em>10.26.0.253</em> и сказать ему, что шлюз будет <em>10.26.0.1</em>.</p>
<p>Перезапускаем сервер:</p>
<pre tabindex="0"><code>
systemctl start openvpn-server@gateway &amp;&amp; systemctl enable openvpn-server@gateway
</code></pre><p><img src="/images/posts/ip_a_server_tun-1024x107.png" alt="Туннель на шлюзе"></p>
<h2 id="openvpn-клиент">OpenVPN-клиент</h2>
<p>Теперь на домашнем сервере необходимо установить как раз соединение со шлюзом. Устанавливаем (напоминаю, у меня <strong>armbian</strong>):</p>
<pre tabindex="0"><code>
apt install openvpn
</code></pre><p>Сразу идем в <strong>/etc/openvpn</strong> и наполняем файл <strong>client.conf</strong> настройками:</p>
<pre tabindex="0"><code>
dev tun
proto tcp
remote  1194
client
resolv-retry infinite
ca /etc/openvpn/client/ca.crt
cert /etc/openvpn/client/apps.crt
key /etc/openvpn/client/apps.key

persist-key
persist-tun
comp-lzo
verb 3
status /var/log/openvpn/openvpn-status.log 1
status-version 3
log-append /var/log/openvpn/openvpn-client.log

tls-auth client/ta.key 1

keepalive 10 120
</code></pre><p>Файл клиента очень похож на файл сервера, просто в нем отличается пара строк. Так же не забываем скопировать с сервера сертификаты для клиента.</p>
<p>Теперь запускаем:</p>
<pre tabindex="0"><code>
systemctl start openvpn@client &amp;&amp; systemctl enable op[envpn@client
</code></pre><p>Обязательно проверяем, что туннель поднялся как на сервере, так и на клиенте с помощью команды <strong>ip a</strong>. В выводе должен появиться сетевой интерфейс <strong>tun</strong> и отобразиться IP-адреса, которые мы установили.</p>
<p><img src="/images/posts/ip_a_client_tun-1024x101.png" alt="Туннель на внутреннем сервере"></p>
<h2 id="dns">DNS</h2>
<p>cloudПеред настройкой web-сервера хочется чтобы это было «<strong>красиво</strong>«. Так что бежим за покупкой доменного имени второго уровня! Проверяем желаемый и, если он свободен, покупаем и радуемся! Сказать тут больше нечего…</p>
<p>После покупки прописываем внешний IP-адрес нашего шлюза и ждем… Ждем… Ждем… Можем и сутки ждать, пока все заработает. Это нормально, так как DNS-запись должна распространиться по всем DNS-серверам.</p>
<p>Если нужен домен 3-го уровня на Вашем же домене 2-го уровня, то добавляем его там же и вешаем на этот же IP-адрес. В этом ничего страшного нет, так как web-сервер сам разрулит какой запрос к какому серверу перенаправить.</p>
<h2 id="nginx-и-certbot">Nginx и CertBot</h2>
<p>Вот тут, по сути, нужно их настраивать в паре. Чтобы настроить получение сертификатов я использовал вот эту <a href="https://habr.com/ru/post/318952/">статью</a>. Работает и на CentOS (а чего бы ему не работать?).</p>
<p>Теперь когда все готово прописываем конфигурацию для нашего сайта:</p>
<pre tabindex="0"><code>
server {
    listen       80;
    server_name   www.;

    access_log  /var/log/nginx/site.access.log  main;

    include acme;

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen      443 ssl http2;
    server_name  www.;

    gzip on;

    access_log /var/log/nginx/site.access.log;

    ssl_certificate /etc/letsencrypt/live//fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live//privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live//chain.pem;

    ssl_stapling on;
    ssl_stapling_verify on;

    ssl_ciphers &#34;EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS&#34;;
    #ssl_ciphers  &#34;RC4:HIGH:!aNULL:!MD5:!kEDH&#34;;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

    ssl_prefer_server_ciphers on;

    ssl_session_cache   shared:SSL:100m;
    ssl_session_timeout 5m;

    add_header Strict-Transport-Security &#39;max-age=604800&#39;;
    add_header Content-Security-Policy &#34;img-src https: data:; upgrade-insecure-requests&#34;;

    resolver 8.8.8.8;

    client_max_body_size 1024m;

    set_real_ip_from 10.26.0.1;
    real_ip_header X-Forwarded-For;

    location / {
        proxy_pass http://10.26.0.253:8080;
        proxy_set_header Host $host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 120s;
        proxy_read_timeout 180s;
    }

    error_page 404              /404.html;

    error_page  500 502 503 504         /50x.html;

}
</code></pre><p>Вот тут нужно немного пояснить.</p>
<p>Тот сервер, что <strong>слушает</strong> 80й порт нужен чтобы ответить на <strong>acme</strong> для <strong>certbot</strong>, а остальные запросы перенаправить на 443й порт. Второй сервер, который <strong>слушает</strong> 443й порт перенаправляет запросы по vpn-каналу на наш домашний сервер. Вот собственно и все.</p>
<p>Теперь проверим правильно ли написана конфигурация (нет ли ошибок):</p>
<pre tabindex="0"><code>
nginx -t
</code></pre><p>И, если все хорошо, скажем <strong>nginx</strong> чтобы применил конфигурацию:</p>
<pre tabindex="0"><code>
nginx -s reload
</code></pre><h2 id="результат">Результат</h2>
<p>Вот и все. Теперь можно спокойно отключать мобильник от домашнего Wi-Fi и пробовать ломиться по нашему адресу.</p>
]]></content>
        </item>
        
        <item>
            <title>Телеграм-бот с нуля (часть 2)</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D1%82%D0%B5%D0%BB%D0%B5%D0%B3%D1%80%D0%B0%D0%BC-%D0%B1%D0%BE%D1%82-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2/</link>
            <pubDate>Thu, 23 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D1%82%D0%B5%D0%BB%D0%B5%D0%B3%D1%80%D0%B0%D0%BC-%D0%B1%D0%BE%D1%82-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2/</guid>
            <description>&lt;p&gt;Я уже писал про &lt;a href=&#34;https://ymnuktech.ru/telegram-bot-from-zero&#34;&gt;телеграм-бот с нуля&lt;/a&gt;. В этот раз я добавлю базу данных, в которой буду хранить сообщения и показывать случайным образом. На самом деле ничего сложного в этом нету, а базы данных не кусаются. Продолжим.&lt;/p&gt;
&lt;h2 id=&#34;материал&#34;&gt;Материал&lt;/h2&gt;
&lt;p&gt;Прежде чем что-то делать нужно что-то иметь. Нам нужен текст. Не буду писать о нем много. Скажу только что сообщения предварительно нужно подготовить. Я сделал 2 файла CSV. Первый файл у меня содержит в первой колонке раздел, а во второй само сообщение. Второй файл будет содержать раздел и его отображаемое сообщение.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про <a href="https://ymnuktech.ru/telegram-bot-from-zero">телеграм-бот с нуля</a>. В этот раз я добавлю базу данных, в которой буду хранить сообщения и показывать случайным образом. На самом деле ничего сложного в этом нету, а базы данных не кусаются. Продолжим.</p>
<h2 id="материал">Материал</h2>
<p>Прежде чем что-то делать нужно что-то иметь. Нам нужен текст. Не буду писать о нем много. Скажу только что сообщения предварительно нужно подготовить. Я сделал 2 файла CSV. Первый файл у меня содержит в первой колонке раздел, а во второй само сообщение. Второй файл будет содержать раздел и его отображаемое сообщение.</p>
<h2 id="база-данных">База данных</h2>
<p>Для начала нужен пакет для работы с БД. Я возьму <a href="https://gorm.io/">GORM</a>. Библиотека может много чего взять на себя, а мы этим воспользуемся. Я буду использовать MariaDB так как она у меня уже есть и новый сервер я разворачивать не собираюсь (а зачем?). Устанавливаем:</p>
<pre tabindex="0"><code>
go get gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/satori/go.uuid
</code></pre><p>Для работы с MariaDB мне понадобится пакет mysql. Так же я буду немного маньячить. Для этого и нужен пакет для работы с UUID. В принципе все готово.</p>
<h2 id="структура-бд">Структура БД</h2>
<p>Для начала создадим базовую структуру, которая будет будет использоваться во всех моделях (<strong>db/models/base.go</strong>):</p>
<pre tabindex="0"><code>
package models

import (
	&#34;time&#34;
	&#34;gorm.io/gorm&#34;
)

type Base struct {
	ID        uint      `gorm:&#34;type:uuid;primary_key;&#34;`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `sql:&#34;index&#34;`
}
</code></pre><p>Эта структура описывает все самое необходимое для моделей. Теперь нам нужны 2е таблицы. Первая <strong>db/models/congrat_cats.go</strong>:</p>
<pre tabindex="0"><code>
package models

type CongratCats struct {
	Base
	Name    string `gorm:&#34;column:name;type:varchar(50);&#34;`
	Display string `gorm:&#34;column:name;type:text&#34;`
}
</code></pre><p>Тут я перечисляю категории поздравлений. Вторая <strong>db/models/congrat_text.go</strong>:</p>
<pre tabindex="0"><code>
package models

type CongratText struct {
	Base
	CongratCatsID uint `gorm:&#34;column:id_congrat_cat&#34;`
	CongratCats   CongratCats
	txt           string `gorm:&#34;column:txt;type:text&#34;`
}
</code></pre><h2 id="подготовка-бд">Подготовка БД</h2>
<p>Прежде чем работать с БД необходимо немного можифицировать код из предыдущей статьи. В файле <strong>structs/config.go</strong> добавим некоторые свойства структуры:</p>
<pre tabindex="0"><code>
type Config struct {
	TBotApiToken string
	Env          string

	MigrateDB           bool
	MigrateDBReferences bool
	DbName              string
	DbAddress           string
	DbPort              string
	DbLogin             string
	DbPassword          string
}
</code></pre><p>И теперь нужно сделать получить все необходимые параметры. Для этого подправим <strong>libs/config.go</strong>:</p>
<pre tabindex="0"><code>
package libs

import (
	&#34;os&#34;
	&#34;tgbot-newyear/structs&#34;
)

var Config structs.Config

func LoadConfig() {
	var exists bool
	Config.TBotApiToken = os.Getenv(&#34;TBOT_API_TOKEN&#34;)

	if Config.Env, exists = os.LookupEnv(&#34;ENV&#34;); !exists {
		Config.Env = &#34;development&#34;
	}

	if Config.DbName, exists = os.LookupEnv(&#34;DB_NAME&#34;); !exists {
		Config.DbName = &#34;bot&#34;
	}

	if Config.DbAddress, exists = os.LookupEnv(&#34;DB_ADDRESS&#34;); !exists {
		Config.DbAddress = &#34;bot&#34;
	}

	if Config.DbLogin, exists = os.LookupEnv(&#34;DB_LOGIN&#34;); !exists {
		Config.DbLogin = &#34;bot&#34;
	}

	if Config.DbPassword, exists = os.LookupEnv(&#34;DB_PASSWORD&#34;); !exists {
		Config.DbPassword = &#34;bot&#34;
	}

	if Config.DbPort, exists = os.LookupEnv(&#34;DB_PORT&#34;); !exists {
		Config.DbPort = &#34;3306&#34;
	}

	prepareArguments()
}

func prepareArguments() {
	args := os.Args[1:]
	for i := range args {
		switch args[i] {
		case &#34;migrate&#34;:
			Config.MigrateDB = true
		}
	}
}
</code></pre><p>Так же добавим вспомогательную функцию в файл <strong>libs/end_transaction.go</strong>:</p>
<pre tabindex="0"><code>
package libs

import &#34;gorm.io/gorm&#34;

func EndTransaction(tx *gorm.DB, err error) (errout error) {
	if err != nil {
		// Откат транзакции, так как получилось неудачно
		errout = tx.Rollback().Error
	} else {
		// Ошабок нет, значит все хорошо. Подтверждаем транзакцию
		tx.Commit()
		errout = nil
	}
	return
}
</code></pre><p>И добавим в <strong>launch.json</strong> переменные окружения:</p>
<pre tabindex="0"><code>
{
  &#34;name&#34;: &#34;Launch Bot&#34;,
  &#34;type&#34;: &#34;go&#34;,
  &#34;request&#34;: &#34;launch&#34;,
  &#34;mode&#34;: &#34;debug&#34;,
  &#34;program&#34;: &#34;${workspaceFolder}/main.go&#34;,
  &#34;env&#34;: {
    &#34;TBOT_API_TOKEN&#34;: &#34;Ваш токен&#34;,
    &#34;DB_ADDRESS&#34;: &#34;localhost&#34;,
    &#34;DB_NAME&#34;: &#34;bot&#34;,
    &#34;DB_LOGIN&#34;: &#34;bot&#34;,
    &#34;DB_PASSWORD&#34;: &#34;bot&#34;
  }
}
</code></pre><p>Далее нам нужно написать немного кода для подключения в базе. Наполняем файл <strong>db/db.go</strong>:</p>
<pre tabindex="0"><code>
package db

import (
	&#34;embed&#34;
	&#34;log&#34;
	&#34;os&#34;
	&#34;time&#34;

	&#34;tgbot-newyear/db/models&#34;
	&#34;tgbot-newyear/libs&#34;

	&#34;gorm.io/driver/mysql&#34;
	&#34;gorm.io/gorm&#34;
	&#34;gorm.io/gorm/logger&#34;
)

//go:embed cats.csv.gz
//go:embed congrats.csv.gz
var f embed.FS

var DB *gorm.DB

func Connect() {
	dsnMySQL := libs.Config.DbLogin + &#34;:&#34; + libs.Config.DbPassword + &#34;@tcp(&#34; + libs.Config.DbAddress + &#34;:&#34; + libs.Config.DbPort + &#34;)/&#34; + libs.Config.DbName + &#34;?charset=utf8mb4&amp;parseTime=True&amp;loc=Local&#34;

	var logLevel logger.LogLevel
	if libs.Config.Env == &#34;production&#34; {
		logLevel = logger.Silent
	} else {
		logLevel = logger.Info
	}

	var err error
	newLogger := logger.New(
		log.New(os.Stdout, &#34;\r\n&#34;, log.LstdFlags), // io writer
		logger.Config{
			SlowThreshold: time.Second, // Slow SQL threshold
			LogLevel:      logLevel,    // Log level
			Colorful:      false,       // Disable color
		},
	)
	newLogger.LogMode(logger.Info)
	DB, err = gorm.Open(mysql.Open(dsnMySQL), &amp;gorm.Config{
		Logger:                                   newLogger,
		DisableForeignKeyConstraintWhenMigrating: true,
	})

	if err != nil {
		panic(err)
	}

	// Установим пул соединений
	dbSettings, err := DB.DB()
	if err != nil {
		panic(err)
	}
	dbSettings.SetMaxIdleConns(10)
	dbSettings.SetMaxOpenConns(50)
	dbSettings.SetConnMaxLifetime(time.Minute * 10)

	if libs.Config.MigrateDB {

		// Создание БД

		DB.AutoMigrate(
			&amp;models.CongratCats{},
			&amp;models.CongratText{},
		)

		if libs.Config.MigrateDBReferences {
			migrateCats()
			migrateCongrats()
		}
	}
}

func migrateCats() {
	// TODO
}

func migrateCongrats() {
	// TODO
}
</code></pre><p>Здесь я настраиваю подключение к базе. Если будет передан параметр <strong>migrate</strong>, то будет создана структура базы либо ее обновление (если она уже есть). В промышленной среде это называется процесс «миграции БД». Так как у нас всего 2 таблицы, то не смысла заморачиваться с чем-то сложным. По этому в данном случае я использую простой вариант.</p>
<p>Теперь, чтобы все работало правильно нужно немного изменить <strong>main.go</strong>:</p>
<pre tabindex="0"><code>
func main() {
	libs.LoadConfig()
	db.Connect()
	if !libs.Config.MigrateDB {
		libs.LoadConfig()
		srv.TGBot()
	}
}
</code></pre><h2 id="обновление-справочников">Обновление справочников</h2>
<p>Помимо создания структуры БД еще необходимо наполнить таблицы данными, из которых будем брать данные. В файле  <strong>db/db.go</strong> я подготовил 2е функции: <strong>migrateCats</strong> и <strong>migrateCongrats</strong>. Вот их и будем использовать. И так, для начала <strong>migrateCats</strong>:</p>
<pre tabindex="0"><code>
func migrateCats() {
	var catsFile fs.File
	var err error

	catsFile, err = f.Open(&#34;cats.csv.gz&#34;)
	if err != nil {
		panic(err)
	}

	defer catsFile.Close()

	var gz *gzip.Reader
	gz, err = gzip.NewReader(catsFile)
	if err != nil {
		panic(err)
	}

	defer gz.Close()

	reader := csv.NewReader(gz)
	reader.Comma = &#39;;&#39;

	var record []string
	z := 0
	fmt.Println(&#34;Update categories&#34;)

	tx := DB.Begin()

	defer func() {
		libs.EndTransaction(tx, err)
	}()

	for {
		record, err = reader.Read()
		if err != nil {
			break
		}

		var cat models.CongratCats
		if res := tx.First(&amp;cat, &#34;name = ?&#34;, record[0]); res.RowsAffected == 0 {
			// Создаем
			cat.Name = record[0]
			cat.Display = record[1]
			if res := tx.Create(&amp;cat); res.RowsAffected == 0 {
				panic(res.Error)
			}
		} else {
			// Обновляем
			cat.Display = record[1]
			if res := tx.Save(&amp;cat); res.RowsAffected == 0 {
				panic(res.Error.Error())
			}
		}
	}

	fmt.Printf(&#34;Finished %d records\n&#34;, z)

	err = nil

}
</code></pre><p>И <strong>migrateCongrats</strong>:</p>
<pre tabindex="0"><code>
func migrateCongrats() {
	var catsFile fs.File
	var err error

	catsFile, err = f.Open(&#34;congrats.csv.gz&#34;)
	if err != nil {
		panic(err)
	}

	defer catsFile.Close()

	var gz *gzip.Reader
	gz, err = gzip.NewReader(catsFile)
	if err != nil {
		panic(err)
	}

	defer gz.Close()

	reader := csv.NewReader(gz)
	reader.Comma = &#39;;&#39;

	var record []string
	fmt.Println(&#34;Update texts&#34;)

	tx := DB.Begin()

	defer func() {
		libs.EndTransaction(tx, err)
	}()

	// Очистим все поздравления. Нам они не нужны
	tx.Delete(&amp;models.CongratText{}, &#34;1 = 1&#34;)

	for {
		record, err = reader.Read()
		if err != nil {
			break
		}

		var cat models.CongratCats
		if res := tx.First(&amp;cat, &#34;name = ?&#34;, record[0]); res.RowsAffected == 0 {
			continue
		}

		// Добавим поздравление

		var text models.CongratText
		text.CongratCatsID = cat.ID
		text.Txt = record[1]

		if res := tx.Create(&amp;text); res.RowsAffected == 0 {
			panic(res.Error)
		}

	}

	err = nil

}
</code></pre><p>Я не стал вводить проверку существующего сообщения в таблице, так как записей не много и выполняется быстро. В академических целях этого достаточно. В реальном же приложении нужно предусмотреть именно добавление и обновление записей, так как это может быть справочная информация и при удалении и новой вставке связи могут быть смещены и потеряны. Так что подбирайте методы в соответствии с требованиями.</p>
<h2 id="поздравления">Поздравления</h2>
<p>Вот теперь мы подошли к реализации результата. В файле <strong>srv/handlers/index.go</strong> пишем следующее:</p>
<pre tabindex="0"><code>
package handlers

import (
	&#34;fmt&#34;
	&#34;math/rand&#34;
	&#34;tgbot-newyear/db&#34;
	&#34;tgbot-newyear/db/models&#34;
	&#34;tgbot-newyear/libs&#34;
	&#34;tgbot-newyear/tglibs&#34;

	tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;
)

func Handlers(event *tgbotapi.Update) (msg *tgbotapi.MessageConfig, err error) {

	if event.CallbackQuery != nil {

		tx := db.DB.Begin()
		defer func() {
			libs.EndTransaction(tx, err)
		}()

		var cats models.CongratCats
		if res := tx.Preload(&#34;CongratText&#34;).First(&amp;cats, &#34;name = ?&#34;, event.CallbackQuery.Data); res.RowsAffected == 0 {
			tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), fmt.Sprintf(&#34;Что вы имели ввиду?&#34;))
			msg = &amp;tmp
			err = res.Error
			return
		}

		if len(cats.CongratText) == 0 {
			tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), fmt.Sprintf(&#34;Извините, но я не знаю что Вам сказать...&#34;))
			msg = &amp;tmp
			return
		}

		num := rand.Int31n(int32(len(cats.CongratText)))

		tmp := tgbotapi.NewMessage(tglibs.GetChatID(event), cats.CongratText[num].Txt)
		tglibs.TGSendMessage(tmp)
	}

	return

}
</code></pre><p>И немного изменим <strong>srv/tgbot.go</strong> функцию <strong>processingMessage</strong>:</p>
<pre tabindex="0"><code>
func processingMessage(update *tgbotapi.Update) {

	var err error
	var message *tgbotapi.MessageConfig

	chatID := tglibs.GetChatID(update)

	message, err = handlers.Handlers(update)

	if err != nil {
		tmp := tgbotapi.NewMessage(chatID, err.Error())
		tglibs.TGSendMessage(tmp)
	} else if message != nil {
		tglibs.TGSendMessage(*message)
	} else {

		tx := db.DB.Begin()
		defer func() {
			libs.EndTransaction(tx, err)
		}()

		var cats []models.CongratCats
		if res := tx.Find(&amp;cats); res.RowsAffected == 0 {
			tmp := tgbotapi.NewMessage(chatID, &#34;Ой! Что-то случилось...&#34;)
			tglibs.TGSendMessage(tmp)
		} else {

			tmp := tgbotapi.NewMessage(chatID, &#34;Выберите действие&#34;)

			var buttons [][]tgbotapi.InlineKeyboardButton
			for i := range cats {
				b := tgbotapi.NewInlineKeyboardButtonData(cats[i].Display, cats[i].Name)
				row := tgbotapi.NewInlineKeyboardRow(b)
				buttons = append(buttons, row)
				if len(buttons) == 100 {
					var kb tgbotapi.InlineKeyboardMarkup
					kb.InlineKeyboard = buttons
					message.ReplyMarkup = kb
					tglibs.TGSendMessage(tmp)
					tmp = tgbotapi.NewMessage(chatID, &#34;Продолжение&#34;)
					buttons = [][]tgbotapi.InlineKeyboardButton{}
				}
			}

			var kb tgbotapi.InlineKeyboardMarkup
			kb.InlineKeyboard = buttons

			tmp.ReplyMarkup = kb
			tglibs.TGSendMessage(tmp)
		}
	}
}
</code></pre><p>Теперь можно запускать.</p>
<p><img src="/images/posts/bot.png" alt="Результат работы бота"></p>
<h2 id="в-заключении">В заключении</h2>
<p>Бот готов и им можно пользоваться. Мой результат получился <a href="http://t.me/send_congrat_bot">вот таким</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Телеграм-бот с нуля</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D1%82%D0%B5%D0%BB%D0%B5%D0%B3%D1%80%D0%B0%D0%BC-%D0%B1%D0%BE%D1%82-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F/</link>
            <pubDate>Mon, 20 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D1%82%D0%B5%D0%BB%D0%B5%D0%B3%D1%80%D0%B0%D0%BC-%D0%B1%D0%BE%D1%82-%D1%81-%D0%BD%D1%83%D0%BB%D1%8F/</guid>
            <description>&lt;p&gt;Про телеграм написано немеренно статей и заметок, а про ботов еще больше. Вся главная документация разработчика есть на &lt;a href=&#34;https://core.telegram.org/bots&#34;&gt;официальном&lt;/a&gt;&lt;a href=&#34;https://core.telegram.org/bots/api&#34;&gt;сайте&lt;/a&gt;. Я постараюсь написать более или менее полезного бота от начала и до конца, чтобы его можно было использовать на практике. Мой пример будет представлять отправку поздравления с «Новым Годом». Приступим.&lt;/p&gt;
&lt;h2 id=&#34;с-чего-начать&#34;&gt;С чего начать&lt;/h2&gt;
&lt;p&gt;Для начала нам нужен действующий Телеграм-клиент. Через него нужно зарегистрировать бота. Для этого нужно воспользоваться ботом. Звучит странно, но так оно и есть. Для этого идем по адресу &lt;a href=&#34;https://core.telegram.org/bots&#34;&gt;https://core.telegram.org/bots&lt;/a&gt; и читаем инструкцию. Нам нужен &lt;a href=&#34;https://t.me/botfather&#34;&gt;BotFather&lt;/a&gt;. Как зарегистрировать бота я рассказывать не буду, так как только ленивый не писал как это сделать. А на официальном сайте прекрасно все написано. После регистрации мы должны получить токен. Он и будет нашей авторизацией для бота.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Про телеграм написано немеренно статей и заметок, а про ботов еще больше. Вся главная документация разработчика есть на <a href="https://core.telegram.org/bots">официальном</a><a href="https://core.telegram.org/bots/api">сайте</a>. Я постараюсь написать более или менее полезного бота от начала и до конца, чтобы его можно было использовать на практике. Мой пример будет представлять отправку поздравления с «Новым Годом». Приступим.</p>
<h2 id="с-чего-начать">С чего начать</h2>
<p>Для начала нам нужен действующий Телеграм-клиент. Через него нужно зарегистрировать бота. Для этого нужно воспользоваться ботом. Звучит странно, но так оно и есть. Для этого идем по адресу <a href="https://core.telegram.org/bots">https://core.telegram.org/bots</a> и читаем инструкцию. Нам нужен <a href="https://t.me/botfather">BotFather</a>. Как зарегистрировать бота я рассказывать не буду, так как только ленивый не писал как это сделать. А на официальном сайте прекрасно все написано. После регистрации мы должны получить токен. Он и будет нашей авторизацией для бота.</p>
<p>Теперь нам нужно немного немного настроить наш проект. Я буду передавать параметры через переменные окружения, по этому, чтобы каждый раз не писать вручную я настраиваю немного проект разработки. В моем случае это <a href="https://code.visualstudio.com/">Visual Studio Code</a>. Вы же можете использовать свой любимы инструмент. Как настраивать саму IDE рассказывать не буду, а только то, что касается проекта. Создаем файл <strong>launch.json</strong> (в разделе «Запуск и отладка») и немного наполняем его:</p>
<pre tabindex="0"><code>
{
    &#34;version&#34;: &#34;0.2.0&#34;,
    &#34;configurations&#34;: [
        {
            &#34;name&#34;: &#34;Launch Bot&#34;,
            &#34;type&#34;: &#34;go&#34;,
            &#34;request&#34;: &#34;launch&#34;,
            &#34;mode&#34;: &#34;debug&#34;,
            &#34;program&#34;: &#34;${workspaceFolder}/main.go&#34;,
            &#34;env&#34;: {
                &#34;TBOT_API_TOKEN&#34;: &#34;Ваш токен&#34;
            }
        }
    ]
}
</code></pre><p>Данная конфигурация говорит о том, что запускать отладку файла <strong>main.go</strong> и передавать некоторые переменные окружения.</p>
<p>Теперь нужно создать настройки для самого компилятора/сборщика и т.д. В консоле с проектом вводим:</p>
<pre tabindex="0"><code>
go mod init tgbot-newyear
</code></pre><p>После нам понадобится пакет для работы с Telegram:</p>
<pre tabindex="0"><code>
go get github.com/go-telegram-bot-api/telegram-bot-api
</code></pre><p>Проект готово. Можно приступать к написанию кода.</p>
<h2 id="код">Код</h2>
<p>Для начала создадим файл <strong>libs/config.go</strong>:</p>
<pre tabindex="0"><code>
package libs

import (
	&#34;os&#34;
	&#34;tgbot-newyear/structs&#34;
)

var Config structs.Config

func LoadConfig() {
	var exists bool
	Config.TBotApiToken = os.Getenv(&#34;TBOT_API_TOKEN&#34;)

	if Config.Env, exists = os.LookupEnv(&#34;ENV&#34;); !exists {
		Config.Env = &#34;development&#34;
	}

	prepareArguments()
}

func prepareArguments() {
	// TODO
}
</code></pre><p>Этот код получает переменные окружения и параметры командной строки и складывает их в определенную структуру. В дальнейщем будем ее дописывать.</p>
<p>И <strong>structs/config.go</strong>:</p>
<pre tabindex="0"><code>
package structs

type Config struct {
	TBotApiToken string
	Env          string
}
</code></pre><p>Это структура для хранения настроек, чтобы можно было дергать параметры там где они нам нужны.</p>
<p>Теперь создадим файл <strong>srv/tgbot.go</strong>:</p>
<pre tabindex="0"><code>
package srv

import (
	&#34;log&#34;
	&#34;tgbot-newyear/libs&#34;
	&#34;tgbot-newyear/srv/handlers&#34;
	&#34;tgbot-newyear/tglibs&#34;
	&#34;tgbot-newyear/tgstructs&#34;

	tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;
)

var Bot *tgbotapi.BotAPI

func TGBot() {

	var err error
	Bot, err = tgbotapi.NewBotAPI(libs.Config.TBotApiToken)
	if err != nil {
		log.Panic(err)
	}

	if libs.Config.Env == &#34;production&#34; {
		Bot.Debug = false
	} else {
		Bot.Debug = true
	}

	log.Printf(&#34;Authorized on account %s&#34;, Bot.Self.UserName)

	u := tgbotapi.NewUpdate(0)
	u.Timeout = 60

	updates, err := Bot.GetUpdatesChan(u)

	if err != nil {
		panic(err)
	}

	tglibs.SetBotContext(Bot)
	tglibs.Msgs = make(chan *tgstructs.Message)
	go tglibs.TGSendChannel(tglibs.Msgs)

	for update := range updates {

		processingMessage(&amp;update)

	}
}

func processingMessage(update *tgbotapi.Update) {

	var err error
	var message *tgbotapi.MessageConfig

	chatID := tglibs.GetChatID(update)

	message, err = handlers.Handlers(update)

	if err != nil {
		tglibs.TGSendMessage(*message)
	} else {
		tmp := tgbotapi.NewMessage(chatID, &#34;Выберите действие&#34;)
		tglibs.TGSendMessage(tmp)
	}

}
</code></pre><p>Здесь мы создаем экземпляр бота и запускаем его.  В функции <strong>TGBot</strong> я устанавливаю все настройки и запускаю цикл обработки. В данном случае поступающие сообщения приходят из канала. Если канал пуст, то цикл будет заморожен, пока не поступит новое сообщение.</p>
<p>После создаем файлы <strong>srv/handlers/index.go</strong>:</p>
<pre tabindex="0"><code>
package handlers

import (
	tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;
)

func Handlers(event *tgbotapi.Update) (msg *tgbotapi.MessageConfig, err error) {

	return

}
</code></pre><p>Это файл-заглушка, в которой пока ничего не происходит. Здесь я буду наполнять бота реакцией чтобы более логически разделить основную часть бота от обработчиков.</p>
<p><strong>tgstructs/message.go</strong>:</p>
<pre tabindex="0"><code>
package tgstructs

import (
	tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;
)

type Message struct {
	Message *tgbotapi.MessageConfig
}
</code></pre><p>Это просто структура сообщения. В данном случае в структуру сообщения будет помещаться сообщение. Таким образом можно добавить другие типы и отправлять их пользователю. Мне понадобятся только текст, но можно отправлять файлы, картинки, видео, заметки, фото и т.д. Таким образом структуру можно расширить.</p>
<p><strong>tglibs/get_chat_id.go</strong>:</p>
<pre tabindex="0"><code>
package tglibs

import tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;

func GetChatID(event *tgbotapi.Update) int64 {
	if event.Message != nil {
		return event.Message.Chat.ID
	} else if event.CallbackQuery != nil {
		return event.CallbackQuery.Message.Chat.ID
	}
	return 0
}
</code></pre><p>Это просто вспомогательная функция для получения ID чата. Он нам понадобится, чтобы мы знали кому отправить сообщение.</p>
<p><strong>tglibs/tgsend.go</strong>:</p>
<pre tabindex="0"><code>
package tglibs

import (
	&#34;reflect&#34;
	&#34;tgbot-newyear/tgstructs&#34;
	&#34;time&#34;

	tgbotapi &#34;github.com/go-telegram-bot-api/telegram-bot-api&#34;
)

var bot *tgbotapi.BotAPI

var Msgs chan *tgstructs.Message

var deferredMessages = make(map[int64]chan *tgstructs.Message)
var lastMessageTimes = make(map[int64]int64)

func SetBotContext(context *tgbotapi.BotAPI) {
	bot = context
}

func TGSendMessage(msg tgbotapi.MessageConfig) {
	var message tgstructs.Message
	message.Message = &amp;msg
	if _, ok := deferredMessages[msg.ChatID]; !ok {
		deferredMessages[msg.ChatID] = make(chan *tgstructs.Message, 1000)
	}
	deferredMessages[msg.ChatID]  0 {
				// Формирование case
				cs := reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
				cases = append(cases, cs)
			}
		}

		if len(cases) &gt; 0 {
			// Достаем одно сообщение из всех каналов
			_, value, ok := reflect.Select(cases)

			if ok {
				msg := value.Interface().(*tgstructs.Message)
				// Выполняем запрос к API
				if msg == nil {
					continue
				}
				if msg.Message != nil {
					if msg.Message.ChatID == 0 {
						continue
					}
					if msg.Message != nil {
						if _, err := bot.Send(msg.Message); err != nil {
							continue
						}
						lastMessageTimes[msg.Message.ChatID] = time.Now().UnixNano()
					}
				}
			}
		}
	}
}

func userCanReceiveMessage(userId int64) bool {
	t, ok := lastMessageTimes[userId]

	return !ok || t+int64(time.Second)
</code></pre><p>Достаточно сложная функция. Здесь мы получаем сообщение и кладем его в очередь. После в отдельном потоке мы эти сообщения читаем из очереди и проверяем время отправки. дело в том что в Telegram есть ограничения на количество отправок сообщений за определенное время. О лимитах можно почитать <a href="https://limits.tginfo.me/ru-RU">здесь</a>. Достаточно наглядная таблица.</p>
<p>Вот в таком варианте можно уже запустить приложение, подключиться к боту и написать ему что-то. На все сообщения он будет отвечать одинаково:</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-12.png" alt="Работа бота"></p>
<h2 id="и-все">И все?</h2>
<p>Пока что да. Такое приложение можно разместить на <a href="https://ymnuktech.ru/tag/home-server/">домашнем сервере</a> или на каком-нибудь внешнем. Далее я буду показывать как его немного оживить. А пока что можно с ним немного <a href="https://t.me/send_congrat_bot">поиграть</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Плагин для WordPress</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD-%D0%B4%D0%BB%D1%8F-wordpress/</link>
            <pubDate>Thu, 16 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD-%D0%B4%D0%BB%D1%8F-wordpress/</guid>
            <description>&lt;p&gt;И так, ранее я писал про &lt;a href=&#34;https://ymnuktech.ru/service-classify-text&#34;&gt;классификацию сообщеий&lt;/a&gt;. Теперь неплохо дыло бы использовать его на практике. Заодно не плохо было бы разобраться как написать пдлагин для WordPress. В данном случае это будет плагин в тени, т.е. он будет проверять сообщения комментариев без участия пользователя.&lt;/p&gt;
&lt;h2 id=&#34;скелет&#34;&gt;Скелет&lt;/h2&gt;
&lt;p&gt;Для начала не плохо было бы понять с чего начать. Я достаточно долго блуждал по интернету в поисках информации. Когда впервые что-то берешь в руки и не знаешь что с этим делом начинаешь искать хоть какую-то вводную статью. Нашел я пару статей, но они не для моей версии (на текущий момент 5.8.2). Хотелось что-то свежее.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>И так, ранее я писал про <a href="https://ymnuktech.ru/service-classify-text">классификацию сообщеий</a>. Теперь неплохо дыло бы использовать его на практике. Заодно не плохо было бы разобраться как написать пдлагин для WordPress. В данном случае это будет плагин в тени, т.е. он будет проверять сообщения комментариев без участия пользователя.</p>
<h2 id="скелет">Скелет</h2>
<p>Для начала не плохо было бы понять с чего начать. Я достаточно долго блуждал по интернету в поисках информации. Когда впервые что-то берешь в руки и не знаешь что с этим делом начинаешь искать хоть какую-то вводную статью. Нашел я пару статей, но они не для моей версии (на текущий момент 5.8.2). Хотелось что-то свежее.</p>
<p>В результате поисков я нашел пару интересных и полезные статей. Но мне повезло еще в одном: <a href="https://github.com/DevinVinson/WordPress-Plugin-Boilerplate/">генератор скелета</a>. Чтобы со всем этим добром не ковыряться ребята <a href="https://wordpresslab.ru/generator-wordpress-plaginov/">выложили форму</a>, в которой можно просто сгенерировать шаблон. Думаю с формой можно разобраться. Там все просто.</p>
<h2 id="подготовка">Подготовка</h2>
<p>Вот мы сгенерировали скелет, скачали, распаковали  в <strong>wp/wp-content/plugins/classify-comments-wp-plugin</strong> (в моем случае). Что дальше? А дальше нужно начинать писать, так как в нем уже все готово.</p>
<p>Для начала открываем основной файл (в моем случае <strong>classify-comment.php</strong>) и правим заголовок:</p>
<pre tabindex="0"><code>
/**
 * The plugin bootstrap file
 *
 * This file is read by WordPress to generate the plugin information in the plugin
 * admin area. This file also includes all of the dependencies used by the plugin,
 * registers the activation and deactivation functions, and defines a function
 * that starts the plugin.
 *
 * @since             1.0.0
 * @package           Classify_Comment
 *
 * @wordpress-plugin
 * Plugin Name:       Classify Comment
 * Plugin URI:        https://ymnuktech.ru
 * Description:       Classify comments for toxic messages
 * Version:           1.0.0
 * Author:            Alexandr Fedoryuk
 * Author URI:        https://ymnuktech.ru
 * License:           GPL-2.0+
 * License URI:       http://www.gnu.org/licenses/gpl-2.0.txt
 * Text Domain:       classify-comment
 * Domain Path:       /languages
 */
</code></pre><p>Эти комментарии нужны чтобы WordPress показал информацию о плагине: кто написал, ссылки, адреса и т.д. Теперь нужно немного подредактировать скелет, так как мне лично не нравится, что я должен дублировать некоторую информацию. Для этого я создал файл <strong>includes/class-classify-comment-ext.php</strong> и добавил следующее:</p>
<pre tabindex="0"><code>
class Classify_Comment_Ext
{

    public const table_classify_comments = &#34;classify_comments&#34;;

    public const table_classify_comments_option = &#34;classify_comments_option&#34;;

    public $address;
    public $cats;

    public function __constructor()
    {
    }

    /**
     * Проверяет переданное сообщение и возвращает его категорию
     * @param string $msg - Сообщение
     * @return string - возвращает категорию
     */
    protected function verifyMessage($msg, $moreInfo = null)
    {
        $this-&gt;loadOptions();

        $data = array(
            &#39;text&#39; =&gt; $msg
        );
        $payload = json_encode($data);

        $ch = curl_init($this-&gt;address . &#34;/api/v1/fisher&#34;);

        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
        curl_setopt($ch, CURLOPT_HTTPHEADER, array(&#39;Content-Type:application/json&#39;));
        # Return response instead of printing.
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        # Send request.
        $result = curl_exec($ch);
        curl_close($ch);
        $result = json_decode($result);

        global $wpdb;

        $q = $wpdb-&gt;prepare(
            &#34;
        INSERT INTO `&#34; . $wpdb-&gt;prefix . Classify_Comment_Ext::table_classify_comments . &#34;`
        (`comment_content`, `cat`, `comment_post_ID`, `comment_author`, `comment_author_email`, `comment_author_url`, `comment_author_IP`, `comment_agent`, `user_ID`)
        VALUES
        (%s, %s, %d, %s, %s, %s, %s, %s, %d)&#34;,
            $msg,
            $result-&gt;data,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_post_ID&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_author&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_author_email&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_author_url&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_author_IP&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;comment_agent&#39;] : null,
            !is_null($moreInfo) ? $moreInfo[&#39;user_ID&#39;] : null
        );

        $wpdb-&gt;query($q);

        if ($result-&gt;result) return $result-&gt;data;

        return null;
    }

    /**
     * Проверка по списку блокировки категории
     * @param string Категория для проверки
     * @param boolean Если попадает в указанную категорию в настройках, то возвращает true
     */
    protected function validateCats($msg, $moreInfo = null)
    {
        $cat = $this-&gt;verifyMessage($msg, $moreInfo);

        // error_log($cat);

        if (count($this-&gt;cats) === 0) return false;

        foreach ($this-&gt;cats as $val) {
            if ($val == $cat) return true;
        }

        return false;
    }

    protected function loadOptions()
    {
        global $wpdb;

        $res = $wpdb-&gt;get_results(&#34;
		SELECT value FROM {$wpdb-&gt;prefix}classify_comments_option WHERE name = &#39;address&#39;&#34;);
        $this-&gt;address = $res[0]-&gt;value;

        $res = $wpdb-&gt;get_results(&#34;
		SELECT value FROM {$wpdb-&gt;prefix}classify_comments_option WHERE name = &#39;cats&#39;&#34;);

        $cats = array();

        if (isset($res) &amp;&amp; count($res) &gt; 0 &amp;&amp; !is_null($res[0]-&gt;value)) {
            $res = explode(&#34;,&#34;, $res[0]-&gt;value);
            if (count($res) &gt; 0) {
                foreach ($res as $val) {
                    $val = trim($val);
                    if (strlen($val) &gt; 0) {
                        array_push($cats, $val);
                    }
                }
            }
        }

        $this-&gt;cats = array_unique($cats);
    }
}
</code></pre><p>После, для классов <strong>Classify_Comment_Admin</strong> и <strong>Classify_Comment_Public</strong> я добавил <strong>extends Classify_Comment_Ext</strong>. Далее чтобы это заработало (иначе упадет в ошибку) в классе <strong>includes/class-classify-comment.php</strong> в функцию <strong>load_dependencies()</strong> добавляем строчку:</p>
<pre tabindex="0"><code>
require_once plugin_dir_path(dirname(__FILE__)) . &#39;includes/class-classify-comment-ext.php&#39;;
</code></pre><p>На этом подготовка закончена.</p>
<h2 id="реализация-админки">Реализация админки</h2>
<p>Первым делом можно создать меню для админки. Для этого добавляем функцию <strong>admin_generate_menu()</strong>:</p>
<pre tabindex="0"><code>
public function admin_generate_menu()
{
	// Добавляем основной раздел меню
	add_menu_page(&#39;Добро пожаловать в модуль классификации комментариев&#39;, &#39;Токсичность&#39;, &#39;manage_options&#39;, &#39;options_toxic_comments&#39;, array(&amp;$this, &#39;admin_options&#39;));;
	// Добавляем дополнительный раздел
	add_submenu_page(&#39;options_toxic_comments&#39;, &#39;Управление содержимом&#39;, &#39;Проверка&#39;, &#39;manage_options&#39;, &#39;toxic_test&#39;, array(&amp;$this, &#39;admin_comment_test&#39;));
	add_submenu_page(&#39;options_toxic_comments&#39;, &#39;Управление содержимом&#39;, &#39;Список сообщений&#39;, &#39;manage_options&#39;, &#39;toxic_list&#39;, array(&amp;$this, &#39;admin_comment_list&#39;));
}
</code></pre><p>В функции <strong>define_admin_hooks()</strong> регистрируем наше меню:</p>
<pre tabindex="0"><code>
$this-&gt;loader-&gt;add_action(&#39;admin_menu&#39;, $plugin_admin, &#39;admin_generate_menu&#39;);
</code></pre><p>Теперь для страниц напишем 3 функции в классе админки:</p>
<pre tabindex="0"><code>
/**
 * Настройка параметров плагина
 */
public function admin_options()
{
	global $wpdb;

	if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
		// Обновляем данные настроек
		$q = $wpdb-&gt;prepare(&#34;
		INSERT INTO {$wpdb-&gt;prefix}classify_comments_option (name, value)
		VALUES (&#39;address&#39; ,%s)
		ON DUPLICATE KEY UPDATE value = %s&#34;, $_POST[&#39;address&#39;], $_POST[&#39;address&#39;]);
		$wpdb-&gt;query($q);

		$q = $wpdb-&gt;prepare(&#34;
		INSERT INTO {$wpdb-&gt;prefix}classify_comments_option (name, value)
		VALUES (&#39;cats&#39;
		, %s)
		ON DUPLICATE KEY UPDATE value = %s&#34;, $_POST[&#39;cats&#39;], $_POST[&#39;cats&#39;]);
		$wpdb-&gt;query($q);
	}

	$this-&gt;loadOptions();

	$options = array(
		&#39;address&#39; =&gt; $this-&gt;address,
		&#39;cats&#39; =&gt; $this-&gt;cats
	);

	require_once(&#39;partials/classify-comment-admin-options.php&#39;);
}

/**
 * Список проверенных сообщений
 */
public function admin_comment_list()
{
	global $wpdb;

	$res = $wpdb-&gt;get_results(&#34;SELECT * FROM {$wpdb-&gt;prefix}classify_comments ORDER BY date_filtered DESC LIMIT 500&#34;);

	require_once(&#39;partials/classify-comment-admin-list.php&#39;);
}

/**
 * Проверка работы сервиса
 */
public function admin_comment_test()
{

	$result = null;

	if ($_SERVER[&#39;REQUEST_METHOD&#39;] === &#39;POST&#39;) {
		$result = $this-&gt;verifyMessage($_POST[&#39;message&#39;]);
	}

	require_once(&#39;partials/classify-comment-admin-test.php&#39;);
}
</code></pre><p>В директории partials размещаем наши файлы представления для отображения информации. <strong>classify-comment-admin-list.php</strong>:</p>
<pre tabindex="0"><code>
    # Список обработанных комментариев

            Автор
            E-Mail
            URL
            IP
            Комментарий
            Агент
            Категория
            Дата фильтрации

                comment_author; ?&gt;
                comment_author_email; ?&gt;
                comment_author_url; ?&gt;
                comment_author_IP; ?&gt;
                comment_content; ?&gt;
                comment_agent; ?&gt;
                cat; ?&gt;
                date_filtered; ?&gt;

    Всего показано строк:
</code></pre><p><strong>classify-comment-admin-options.php</strong>:</p>
<pre tabindex="0"><code>
    # Настройки Токсичности

            Адрес обработчика
            &#34;&gt;

            Категории для блокировки (через запятую)
            &#34;&gt;
</code></pre><p><strong>classify-comment-admin-test.php</strong>:</p>
<pre tabindex="0"><code>
        Введите сообщение для проверки
        &#34;&gt;

            Результат:
</code></pre><p>С админкой закончили.</p>
<h2 id="основной-функционал">Основной функционал</h2>
<p>Теперь не плохо было бы реализовать саму фильтрацию. В классе <strong>Classify_Comment_Public</strong> добавим функцию:</p>
<pre tabindex="0"><code>
/**
 * Проверяем комментарий на токсичность. Если это будет группа из списка настроек, то вернем результат &#39;spam&#39; чтобы заблокировать.
 */
public function pre_filter_comment($approved, $commentdata)
{
	if ($approved === &#39;spam&#39;) return $approved;

	if ($this-&gt;validateCats($commentdata[&#39;comment_content&#39;], $commentdata) === true) return &#39;spam&#39;;

	return $approved;
}
</code></pre><p>А в функцию <strong>define_public_hooks()</strong> добавим хук:</p>
<pre tabindex="0"><code>
$this-&gt;loader-&gt;add_filter(&#39;pre_comment_approved&#39;, $plugin_public, &#39;pre_filter_comment&#39;, 10, 2);
</code></pre><p>Тут все просто, потому как основной функционал уже реализован в классе <strong>Classify_Comment_Ext</strong>.</p>
<h2 id="последний-штрих">Последний штрих</h2>
<p>Еще нужно реализовать маленькую функцию удаления плагина. В файле <strong>uninstall.php</strong> запишем следующее:</p>
<pre tabindex="0"><code>
if (!defined(&#39;WP_UNINSTALL_PLUGIN&#39;)) {
	global $wpdb;
	wpdb-&gt;query(&#34;DROP TABLE IF EXISTS {$wpdb-&gt;prefix}classify_comments&#34;);
	wpdb-&gt;query(&#34;DROP TABLE IF EXISTS {$wpdb-&gt;prefix}.classify_comments_option&#34;);
	exit;
}
</code></pre><p>Теперь все готово и можно активировать плагин.</p>
<h2 id="что-получилось">Что получилось</h2>
<p>В админке должно появиться 3 страницы. Заходим в <strong>Токсичность</strong> и указываем адрес до сервиса и через запятую перечисляем категории, которые будем блокировать.</p>
<p><img src="/images/posts/image.png" alt="Настройка плагина"></p>
<p>В меню проверки можем написать текст и посмотреть как оно работает.</p>
<p><img src="/images/posts/image-1.png" alt="Проверка сообщения"></p>
<p>А в списке сообщений можем посмотреть историю.</p>
<p><img src="/images/posts/image-2-1024x132.png" alt="История обработки"></p>
<p>Исходный код можно взять из <a href="https://gitlab.com/ymnukus/classify-comments-wp-plugin">гита</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Сервис классификации сообщений</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8-%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9/</link>
            <pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81-%D0%BA%D0%BB%D0%B0%D1%81%D1%81%D0%B8%D1%84%D0%B8%D0%BA%D0%B0%D1%86%D0%B8%D0%B8-%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9/</guid>
            <description>&lt;p&gt;Мне очень захотелось сделать спам-фильтр, но с чего начать я не знал. Чисто случайно наткнулся я на книгу «Программируем коллективный разум». В частности в «Главе 6» написана как раз про данные алгоритмы. Вот их я и решил попробовать реализовать. А чтобы было интереснее, а не простое перенабивание кода, я решил сделать простой сервис классификации сообщений.&lt;/p&gt;
&lt;h2 id=&#34;реализация&#34;&gt;Реализация&lt;/h2&gt;
&lt;p&gt;В книге написано как реализовать 2 алгоритма: Наивный метод (Байесовский) и метод Фишера. Что это за алгоритмы и как они работают прекрасно все расписано. Так же в ней написана реализация на Python, но… Мне захотелось попробовать сделать на другом языке программирования и выбрал я Golang.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Мне очень захотелось сделать спам-фильтр, но с чего начать я не знал. Чисто случайно наткнулся я на книгу «Программируем коллективный разум». В частности в «Главе 6» написана как раз про данные алгоритмы. Вот их я и решил попробовать реализовать. А чтобы было интереснее, а не простое перенабивание кода, я решил сделать простой сервис классификации сообщений.</p>
<h2 id="реализация">Реализация</h2>
<p>В книге написано как реализовать 2 алгоритма: Наивный метод (Байесовский) и метод Фишера. Что это за алгоритмы и как они работают прекрасно все расписано. Так же в ней написана реализация на Python, но… Мне захотелось попробовать сделать на другом языке программирования и выбрал я Golang.</p>
<p>Сам алгоритм и классы практически переписываются один в один (строчка в строку). Тут ничего сверхординарного нет. Все работает.</p>
<pre tabindex="0"><code>
type Classifier struct {
	Words       map[string]*Word   `json:&#34;words&#34;`
	Cats        map[string]int     `json:&#34;cats&#34;`
	Thresholds  map[string]float32 `json:&#34;thresholds&#34;`
	Minimums    map[string]float32 `json:&#34;minimums&#34;`
	getFeatures func(str string) (result []string, err error)
}

func NewClassifier(f func(str string) (result []string, err error)) *Classifier {
	classifier := Classifier{}
	classifier.initclassifier(f)
	return &amp;classifier
}

func (classifier *Classifier) initclassifier(f func(str string) (result []string, err error)) {
	classifier.getFeatures = f
	classifier.Cats = make(map[string]int)
	classifier.Words = make(map[string]*Word)
	classifier.Thresholds = make(map[string]float32)
	classifier.Minimums = make(map[string]float32)
}

func (classifier *Classifier) SetThreshold(cat string, t float32) {
	classifier.Thresholds[cat] = t
}

func (classifier *Classifier) GetThreshold(cat string) float32 {
	if _, ok := classifier.Thresholds[cat]; !ok {
		return 1.0
	}
	return classifier.Thresholds[cat]
}

// Тренировка
func (classifier *Classifier) Train(str string, cat string) {

	if len(cat) &gt; 0 {

		words, err := classifier.getFeatures(str)
		if err != nil {
			log.Println(err)
			return
		}

		if len(words) &gt; 0 {
			for _, word := range words {
				classifier.incf(word, cat)
			}
			classifier.incc(cat)
		}

	}

}

// Сколько образцов отнесено к данной категории
func (classifier *Classifier) catcount(cat string) int {
	if _, ok := classifier.Cats[cat]; !ok {
		return 0
	}
	return classifier.Cats[cat]
}

// Общее число образцов
/*func (classifier *Classifier) totalcount() int {
	sum := 0
	if len(classifier.Cats) &gt; 0 {
		for _, val := range classifier.Cats {
			sum += val
		}
	}
	return sum
}*/

// Список всех категорий
func (classifier *Classifier) Categories() []string {
	res := make([]string, 0, len(classifier.Cats))
	for k := range classifier.Cats {
		res = append(res, k)
	}
	return res
}

// Увеличить счетчик применений категории
func (classifier *Classifier) incc(cat string) {
	if _, ok := classifier.Cats[cat]; ok {
		classifier.Cats[cat]++
	} else {
		classifier.Cats[cat] = 1
	}
}

// Увеличить счетчик пар признак/категория
func (classifier *Classifier) incf(word string, cat string) {
	if _, ok := classifier.Words[word]; !ok {
		classifier.Words[word] = &amp;Word{}
	}
	classifier.Words[word].IncCat(cat)
}

// Сколько раз признак появлялся в данной категории
func (classifier *Classifier) fcount(word string, cat string) int {
	if _, ok := classifier.Words[word]; ok {
		if _, ok := classifier.Words[word].Categories[cat]; ok {
			return classifier.Words[word].Categories[cat]
		}
	}
	return 0
}

// Условная вероятность
func (classifier *Classifier) fprob(word string, cat string) float32 {
	//if len(docclass.cats) == 0 {
	if classifier.catcount(cat) == 0 {
		return 0.0

	}
	tmp := float32(classifier.fcount(word, cat)) / float32(classifier.catcount(cat))
	return tmp
}

func (classifier *Classifier) Sampletrain() {
	classifier.Train(&#34;Клара у Карла украла караллы.&#34;, &#34;good&#34;)
	classifier.Train(&#34;Шла Саша по шоссе и сосала сушку.&#34;, &#34;good&#34;)
	classifier.Train(&#34;Я вычислю тебя по IP!&#34;, &#34;bad&#34;)
	classifier.Train(&#34;Вычислю тебя&#34;, &#34;bad&#34;)
}

type WeighteProbeStruct struct {
	Word   string
	Cat    string
	Prf    func(word string, cat string) float32
	Weight float32
	Ap     float32
}

// Взвешенная вероятность
func (classifier *Classifier) weighteprobe(weighteProbeArgs WeighteProbeStruct) float32 {
	if weighteProbeArgs.Weight == 0.0 {
		weighteProbeArgs.Weight = 1.0
	}
	if weighteProbeArgs.Ap == 0 {
		weighteProbeArgs.Ap = 0.5
	}

	basicprob := weighteProbeArgs.Prf(weighteProbeArgs.Word, weighteProbeArgs.Cat)

	totals := 0

	for _, categ := range classifier.Categories() {
		totals += classifier.fcount(weighteProbeArgs.Word, categ)
	}

	bp := ((weighteProbeArgs.Weight * weighteProbeArgs.Ap) + (float32(totals) * basicprob)) / (weighteProbeArgs.Weight + float32(totals))
	return bp
}

func (classifier *Classifier) SetMininun(cat string, val float32) {
	classifier.Minimums[cat] = val
}

func (classifier *Classifier) GetMininun(cat string) float32 {
	val, ok := classifier.Minimums[cat]
	if ok {
		return val
	}

	return 0.0
}

func (classifier *Classifier) Save() (res string, err error) {
	var bytes []byte
	bytes, err = json.Marshal(classifier)
	if err != nil {
		return
	}
	res = string(bytes)
	return
}

func (classifier *Classifier) Load(data string) (err error) {
	err = json.Unmarshal([]byte(data), classifier)
	if err != nil {
		return
	}
	return
}

// Вероятность того, что образец с указанным признаком принадлежит указанной категории, в предположении, что в каждой категории будет одинаковое число образцов
func (fisher *Classifier) cprob(word string, cat string) float32 {
	clf := fisher.fprob(word, cat)
	if clf == 0 {
		return 0.0
	}
	var freqsum float32
	for _, c := range fisher.Categories() {
		freqsum += fisher.fprob(word, c)
	}
	return clf / freqsum
}

func (fisher *Classifier) fisherprobe(str string, cat string) float32 {
	p := float32(1.0)
	words, err := fisher.getFeatures(str)
	if err != nil {
		log.Println(err)
		return 0.0
	}
	for _, word := range words {
		p *= fisher.weighteprobe(WeighteProbeStruct{
			Word: word,
			Cat:  cat,
			Prf:  fisher.cprob,
		})
	}
	fscore := float32(-2.0 * math.Log(float64(p)))
	return fisher.invchi2(fscore, len(words)*2)
}

func (fisher *Classifier) invchi2(chi float32, df int) float32 {
	m := chi / 2.0
	sum := float32(math.Exp(float64(-1.0 * m)))
	term := sum
	for i := 1; i  fisher.GetMininun(cat) &amp;&amp; p &gt; max {
			best = cat
			max = p
		}
	}
	return best
}

func (naivebayes *Classifier) docprobe(str string, cat string) float32 {
	p := float32(1.0)
	words, err := naivebayes.getFeatures(str)
	if err != nil {
		log.Println(err)
		return 0
	}
	for _, word := range words {
		p *= naivebayes.weighteprobe(WeighteProbeStruct{
			Word: word,
			Cat:  cat,
			Prf:  naivebayes.fprob,
		})
	}
	return p
}

func (naivebayes *Classifier) Probe(str string, cat string) float32 {
	catprob := naivebayes.catcount(cat)
	docprobe := naivebayes.docprobe(str, cat)
	return docprobe * float32(catprob)
}

func (naivebayes *Classifier) Classify(str string, def string) string {
	probs := make(map[string]float32)
	max := float32(0.0)
	var best string
	for _, cat := range naivebayes.Categories() {
		probs[cat] = naivebayes.Probe(str, cat)
		if probs[cat] &gt; max {
			max = probs[cat]
			best = cat
		}
	}

	for cat := range probs {
		if cat == best {
			continue
		}
		if probs[cat]*naivebayes.GetThreshold(best) &gt; probs[best] {
			return def
		}
	}
	return best
}
</code></pre><p>Так же у нас есть структура для групп слов:</p>
<pre tabindex="0"><code>
type Word struct {
	Categories map[string]int `json:&#34;categories&#34;`
}

func (word *Word) IncCat(cat string) {
	if word.Categories == nil {
		word.Categories = make(map[string]int)
	}
	(*word).Categories[cat]++
}
</code></pre><p>В книге написано как подготовить входящее сообщение для дальнейшей работы. Алгоритм достаточно прост: разделяем текст на слова с удалением всех знаков препинания и пробелов, составляем массив слов и убираем дублирующие слова, затем прогоняем через цикл и убираем все слова, которые меньше либо равно 2ум символам и больше либо равно 20 символам. Все просто. Но мне не хватило чего-то…</p>
<p>Немного покопавшись в своем сознании я вспомнил, что в Python есть пакет NLTK, который отвечает за обработку естественного языка. В частности в нем есть <a href="https://ru.wikipedia.org/wiki/%D0%A1%D1%82%D0%B5%D0%BC%D0%BC%D0%B8%D0%BD%D0%B3">стеммер</a>. В кратце это такая штука, которая ищет основу слова для заданного слова. К примеру «Саша» может быть записано «Саше», «Сашей», «Сашка», «Сашочек» и т.д. В общем вариантов масса. Так вот вероятность того что одно и тоже слово будет повторять начинает постепенно снижаться. Чтобы обучить нашу модель нуже достаточно большой <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D1%80%D0%BF%D1%83%D1%81_%D1%82%D0%B5%D0%BA%D1%81%D1%82%D0%BE%D0%B2">корпус</a> обучения. Для снижения такого объема и для улучшения качества работы модели можно попробовать применить стеммер.</p>
<p>Библиотека называется <a href="http://snowball.tartarus.org">SnowBall</a> и написана она на С. Есть и для Go, но хотелось еще что-то. В общем нашел я <a href="https://github.com/tebeka/snowball">другую библиотеку</a> и мне она подошла. По факту она так же использует Snowball, только выполнено в виде пакета.</p>
<p>Теперь давайте приведем разбиение текста на слова:</p>
<pre tabindex="0"><code>
/// Подготовка текста и разбивка на слова
func GetWords(str string, lang string) (result []string, err error) {
	// Подготовим полученный текст
	var stemmer *snowball.Stemmer
	stemmer, err = snowball.New(lang)
	if err != nil {
		return
	}
	var reSplitWords *regexp.Regexp
	switch lang {
	case &#34;ru&#34;:
		reSplitWords = regexp.MustCompile(`[^А-Яа-я]+`)
	case &#34;en&#34;:
		reSplitWords = regexp.MustCompile(`[^A-Za-z]+`)
	}
	split := reSplitWords.Split(strings.ToLower(str), -1)
	splitted := make(map[string]*string)

	for _, word := range split {
		word = stemmer.Stem(word)
		if len([]rune(word)) &gt; 2 &amp;&amp; len([]rune(word))
</code></pre><p>Использование:</p>
<pre tabindex="0"><code>
сlassifier := structs.NewClassifier(func(str string) (result []string, err error) {
	return GetWords(str, Config.Lang)
})
</code></pre><h2 id="тренировка">Тренировка</h2>
<p>Наша задача вызвать функцию <strong>Train</strong> и передать ей 2 параметра: сообщение и группа, к которой относится сообщение. Вот функция:</p>
<pre tabindex="0"><code>
// Тренировка данных
func train(classifier *structs.Classifier) {
	// TODO
	f, err := os.Open(libs.Config.TraintSetPath)
	if err != nil {
		panic(err)
	}
	defer f.Close()

	csvReader := csv.NewReader(f)

	for {
		row, err := csvReader.Read()
		if err != nil {
			if err == io.EOF {
				err = nil
			}
			return
		}
		classifier.Train(row[0], row[1])
	}
}
</code></pre><h2 id="сохранение-и-загрузка">Сохранение и загрузка</h2>
<p>Сохранять и загружать подготовленные данные можно по разному. Так можно использовать СУБД, а можно напрямую в файлы. Я решил сохранять в файл в формат JSON. Собственно сохранение:</p>
<pre tabindex="0"><code>
fo, err := os.Create(fmt.Sprintf(&#34;./database/%s.json&#34;, libs.Config.Lang))
		if err != nil {
			panic(err)
		}
		defer func() {
			if err := fo.Close(); err != nil {
				panic(err)
			}
		}()

		var j string
		j, err = libs.Config.Classifier.Save()
		if err != nil {
			panic(err)
		}
		fo.WriteString(j)
</code></pre><p>Загрузка:</p>
<pre tabindex="0"><code>
if _, err := os.Stat(fmt.Sprintf(&#34;./database/%s.json&#34;, Config.Lang)); !errors.Is(err, os.ErrNotExist) {
		fi, err := os.Open(fmt.Sprintf(&#34;./database/%s.json&#34;, Config.Lang))
		if err != nil {
			log.Fatal(err)
		}
		defer func() {
			if err = fi.Close(); err != nil {
				log.Fatal(err)
			}
		}()

		b, err := ioutil.ReadAll(fi)
		if err != nil {
			panic(err)
		}
		err = Config.Classifier.Load(string(b))
		if err != nil {
			panic(err)
		}
	}
</code></pre><h2 id="сервис">Сервис</h2>
<p>Для работы с программой как с сервисом сделаем для него HTTP-сервер и привяжем функции. Я буду использовать пакет Echo. Подготавливаем:</p>
<pre tabindex="0"><code>
e := echo.New()
e.Use(middleware.BodyLimit(&#34;1M&#34;))
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.POST(&#34;/api/v1/naivebayes&#34;, naivebayes)
e.POST(&#34;/api/v1/fisher&#34;, fisher)

func naivebayes(c echo.Context) error {
	var err error
	var text struct {
		Text string `json:&#34;text&#34;`
	}
	err = json.NewDecoder(c.Request().Body).Decode(&amp;text)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, structs.Result{
			Result:  false,
			Code:    http.StatusInternalServerError,
			Message: libs.PointerString(&#34;Не удалось прочитать запрос&#34;),
		})
	}

	return c.JSON(http.StatusOK, structs.Result{
		Result: true,
		Code:   http.StatusOK,
		Data:   libs.PointerString(libs.Config.Classifier.Classify(text.Text, &#34;unknow&#34;)),
	})
}

func fisher(c echo.Context) error {
	var err error
	var text struct {
		Text string `json:&#34;text&#34;`
	}
	err = json.NewDecoder(c.Request().Body).Decode(&amp;text)
	if err != nil {
		return c.JSON(http.StatusInternalServerError, structs.Result{
			Result:  false,
			Code:    http.StatusInternalServerError,
			Message: libs.PointerString(&#34;Не удалось прочитать запрос&#34;),
		})
	}

	return c.JSON(http.StatusOK, structs.Result{
		Result: true,
		Code:   http.StatusOK,
		Data:   libs.PointerString(libs.Config.Classifier.FisherClassify(text.Text, &#34;unknow&#34;)),
	})
}
</code></pre><p>И моя функция конфигурации:</p>
<pre tabindex="0"><code>
type Result struct {
	Result  bool    `json:&#34;result&#34;`
	Code    int     `json:&#34;code&#34;`
	Message *string `json:&#34;message&#34;`
	Data    *string `json:&#34;data&#34;`
}

type Config struct {
	IsTrain       bool
	TraintSetPath string

	Lang string

	IsNaivebayes bool
	IsFisher     bool

	Port int

	Classifier *Classifier
}

func LoadConfig() {
	var exists bool
	Config.Lang = &#34;ru&#34;

	var tmp string

	if tmp, exists = os.LookupEnv(&#34;PORT&#34;); exists {
		var err error
		Config.Port, err = strconv.Atoi(tmp)
		if err != nil {
			panic(err)
		}
	} else {
		Config.Port = 3000
	}

	prepareArguments(os.Args[1:])

	Config.Classifier = structs.NewClassifier(func(str string) (result []string, err error) {
		return GetWords(str, Config.Lang)
	})

	if _, err := os.Stat(fmt.Sprintf(&#34;./database/%s.json&#34;, Config.Lang)); !errors.Is(err, os.ErrNotExist) {
		fi, err := os.Open(fmt.Sprintf(&#34;./database/%s.json&#34;, Config.Lang))
		if err != nil {
			log.Fatal(err)
		}
		defer func() {
			if err = fi.Close(); err != nil {
				log.Fatal(err)
			}
		}()

		b, err := ioutil.ReadAll(fi)
		if err != nil {
			panic(err)
		}
		err = Config.Classifier.Load(string(b))
		if err != nil {
			panic(err)
		}
	}
}

func prepareArguments(args []string) {
	if len(args) &gt; 0 {
		switch args[0] {
		case &#34;train&#34;:
			Config.IsTrain = true
			Config.TraintSetPath = args[1]
			prepareArguments(args[2:])
		case &#34;lang&#34;:
			Config.Lang = args[1]
			prepareArguments(args[2:])
		case &#34;--naivebayes&#34;:
			Config.IsNaivebayes = true
			prepareArguments(args[1:])
		case &#34;--fisher&#34;:
			Config.IsFisher = true
			prepareArguments(args[1:])
		}
	}
}
</code></pre><p>Собственно все.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-9-1024x65.png" alt="Пример работы запросов и выдачи результатов.">Пример работы</p>
<h2 id="где-можно-применить">Где можно применить</h2>
<p>Такой сервис можно разместить у себя на <a href="https://ymnuktech.ru/home_server_hardware/">маленьком сервере</a> и использоваться для фильтрации, например, чатов чтобы выявлять оскорбляющих и добавлять блокировки. То что у меня получилось можно посмотреть <a href="https://gitlab.com/ymnukus/classifier-text">тут</a>.</p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер (свое облако)</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B5-%D0%BE%D0%B1%D0%BB%D0%B0%D0%BA%D0%BE/</link>
            <pubDate>Thu, 09 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B2%D0%BE%D0%B5-%D0%BE%D0%B1%D0%BB%D0%B0%D0%BA%D0%BE/</guid>
            <description>&lt;p&gt;Я уже писал про &lt;a href=&#34;https://ymnuktech.ru/tag/home-server/&#34;&gt;домашний сервер&lt;/a&gt;. Настало время разобраться с облаком. Мне хотелось что-то на подобии «Яндекс диска» или «Google Drive». К счастью такая возможность есть и это &lt;a href=&#34;https://ru.wikipedia.org/wiki/Nextcloud&#34;&gt;Nextcloud&lt;/a&gt;. Так же немного познакомимся с Docker и настроим службу в Linux. Приступим.&lt;/p&gt;
&lt;h2 id=&#34;подготовка&#34;&gt;Подготовка&lt;/h2&gt;
&lt;p&gt;Для начала необходимо подготовить площадку. Так как у меня по сути 2 сервера, то я буду разворачивать приложение, так сказать, на сервер приложений. Чтобы удобно было это делать я буду всю структуру устанавливать в контейнерах. Я про это уже &lt;a href=&#34;https://ymnuktech.ru/sharing-res-and-virt&#34;&gt;писал&lt;/a&gt; в общих чертах, а теперь буду использовать.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про <a href="https://ymnuktech.ru/tag/home-server/">домашний сервер</a>. Настало время разобраться с облаком. Мне хотелось что-то на подобии «Яндекс диска» или «Google Drive». К счастью такая возможность есть и это <a href="https://ru.wikipedia.org/wiki/Nextcloud">Nextcloud</a>. Так же немного познакомимся с Docker и настроим службу в Linux. Приступим.</p>
<h2 id="подготовка">Подготовка</h2>
<p>Для начала необходимо подготовить площадку. Так как у меня по сути 2 сервера, то я буду разворачивать приложение, так сказать, на сервер приложений. Чтобы удобно было это делать я буду всю структуру устанавливать в контейнерах. Я про это уже <a href="https://ymnuktech.ru/sharing-res-and-virt">писал</a> в общих чертах, а теперь буду использовать.</p>
<p>И так, нам понадобится система контейнеризации. Для этого буду использовать Docker. Как его устанавливать, настраивать, использовать написано предостаточно. Все будет происходить стандартным способом. Идем на <a href="https://docs.docker.com/engine/">сайт</a>, читаем, изучаем, выбираем свой дистрибутив и устанавливаем по инструкции. Так как у меня Armbian на основе Debian, то я выбираю его и просто все делаю по инструкции.</p>
<p>После этого нам понадобится Docker Compose. Есть так же <a href="https://docs.docker.com/compose/install/">инструкция</a> по его установке. Но не все так просто! У меня он не заработал. Это все потому, что по умолчанию его бинарная версия собрана для систем x86_64, а на одноплатнике arm64. Это совершенно разные архитектуры. Но отчаиваться не будем. Заходим в консоль и устанавливаем из репозиториев:</p>
<pre tabindex="0"><code>
apt install -y docker-compose
</code></pre><p>Версия не последней свежести, но рабочая. Нам этого будет достаточно.</p>
<h2 id="установка">Установка</h2>
<p>Теперь нужно создать директории, которые будут использоваться. Для этого выполняем:</p>
<pre tabindex="0"><code>
mkdir -p /mnt/nfs/cloud/cloud
mkdir -p /mnt/nfs/cloud/db
mkdir -p /opt/nextcloud
</code></pre><p>Так как у меня подключен сетевой диск по nfs, то создаю я директории именно на нем. Третья команда создает директорию в <strong>/opt</strong>. В ней я буду располагать файлы работы сервисов.</p>
<p>Далее выполняю nano <strong>/opt/nextcloud/docker-compose.yml</strong> и наполняю его:</p>
<pre tabindex="0"><code>
version: &#34;2.4&#34;

services:
  db:
    image: mariadb:10.7.1-focal
    command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW --innodb-file-per-table=1 --skip-innodb-read-only-compressed
    restart: always
    environment:
      MARIADB_DATABASE: &#34;имя БД&#34;
      MARIADB_USER: &#34;имя пользователя для БД&#34;
      MARIADB_PASSWORD: &#34;пароль БД&#34;
      MARIADB_RANDOM_ROOT_PASSWORD: 1
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/db
        target: /var/lib/mysql
    mem_limit: 256m
    mem_reservation: 64m

  nextcloud:
    image: nextcloud:22.2.3-fpm-alpine
    restart: always
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/cloud
        target: /var/www/html
    mem_limit: 256m
    mem_reservation: 64m
    links:
      - db
    depends_on:
      - db

  nginx:
    image: nginx:1.20.1-alpine
    restart: always
    volumes:
      - type: bind
        source: /mnt/nfs/cloud/cloud
        target: /var/www/html
      - type: bind
        source: ./nginx.conf
        target: /etc/nginx/nginx.conf
    ports:
      - &#34;8082:80&#34;
    links:
      - nextcloud
    depends_on:
      - nextcloud
    mem_limit: 128m
    mem_reservation: 32m
</code></pre><p>Этот файл описывает какие образы контейнеров необходимо использовать, как их запускать, как объединять их для взаимодействия по сети и как установить ограничение ресурсов. Все необходимы контейнеры можно найти по адресу <a href="https://hub.docker.com">hub.docker.com</a>. В данном случае мне нужно 3 контейнера: nextcloud, mariadb и nginx.</p>
<p>Теперь выполняем <strong>nano /opt/nextcloud/nginx.conf</strong>:</p>
<pre tabindex="0"><code>
worker_processes auto;
pid /run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 15;
    types_hash_max_size 2048;
    server_tokens off;

    include /etc/nginx/mime.types;
    default_type text/javascript;

    access_log off;
    error_log /var/log/nginx/error.log;

    gzip on;
    gzip_min_length 100;
    gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    client_max_body_size 8M;

upstream php-handler {
    server nextcloud:9000;
}

server {
    listen 80;
    listen [::]:80;

    add_header Referrer-Policy &#34;no-referrer&#34; always;
    add_header X-Content-Type-Options &#34;nosniff&#34; always;
    add_header X-Download-Options &#34;noopen&#34; always;
    add_header X-Frame-Options &#34;SAMEORIGIN&#34; always;
    add_header X-Permitted-Cross-Domain-Policies &#34;none&#34; always;
    add_header X-Robots-Tag &#34;none&#34; always;
    add_header X-XSS-Protection &#34;1; mode=block&#34; always;

    fastcgi_hide_header X-Powered-By;

    root /var/www/html;

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location = /.well-known/carddav {
      return 301 $scheme://$host:$server_port/remote.php/dav;
    }
    location = /.well-known/caldav {
      return 301 $scheme://$host:$server_port/remote.php/dav;
    }

    client_max_body_size 512M;
    fastcgi_buffers 64 4K;

    gzip on;
    gzip_vary on;
    gzip_comp_level 4;
    gzip_min_length 256;
    gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    location / {
        rewrite ^ /index.php;
    }

    location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ {
        deny all;
    }
    location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) {
        deny all;
    }

    location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy)\.php(?:$|\/) {
        fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
        set $path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_param HTTPS on;
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass php-handler;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
        fastcgi_read_timeout 300;
    }

    location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
        try_files $uri/ =404;
        index index.php;
    }

   gzip_proxied expired no-cache no-store private no_last_modified no_etag auth;
    gzip_types application/atom+xml application/javascript application/json application/ld+json application/manifest+json application/rss+xml application/vnd.geo+json application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/bmp image/svg+xml image/x-icon text/cache-manifest text/css text/plain text/vcard text/vnd.rim.location.xloc text/vtt text/x-component text/x-cross-domain-policy;

    location / {
        rewrite ^ /index.php;
    }

    location ~ ^\/(?:build|tests|config|lib|3rdparty|templates|data)\/ {
        deny all;
    }
    location ~ ^\/(?:\.|autotest|occ|issue|indie|db_|console) {
        deny all;
    }

    location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+|.+\/richdocumentscode\/proxy)\.php(?:$|\/) {
        fastcgi_split_path_info ^(.+?\.php)(\/.*|)$;
        set $path_info $fastcgi_path_info;
        try_files $fastcgi_script_name =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $path_info;
        fastcgi_param HTTPS on;
        fastcgi_param modHeadersAvailable true;
        fastcgi_param front_controller_active true;
        fastcgi_pass php-handler;
        fastcgi_intercept_errors on;
        fastcgi_request_buffering off;
        fastcgi_read_timeout 300;
    }

    location ~ ^\/(?:updater|oc[ms]-provider)(?:$|\/) {
        try_files $uri/ =404;
        index index.php;
    }
}

}
</code></pre><p>Это конфигурационный файл для nginx.</p>
<p>Дело в том что сам Nextcloud написан на PHP и основной web-сервер, накотором он работает — это Apache. Но так же может работать в связка php-fpm + nginx. Ну мне так хочется и для этого есть определенный ряд причин, который приходит с опытом. А раз есть образ Nextcloud с php-fpm, то почему бы им не воспользоваться?..</p>
<p>Далее переходим в нужную директорию (если еще не перешли) с помощью <strong>cd /opt/nextcloud</strong> и все это добро запускаем:</p>
<pre tabindex="0"><code>
docker-compose -f docker-compose.yml up
</code></pre><p>Теперь наблюдаем и ждем, как загружаются и запускаются контейнеры. На это может потребоваться достаточно времени, так как распаковка образов происходит на SD-карту, а она не особо быстрая.</p>
<p>После того как все загрузиться и запустится открываем у себя на компьютере локально браузер и вводим адрес <strong>http://:8082</strong>. После все делаем поэтапно: отвечаем на вопросы и наблюдаем за действиями системы. Важно также понимать, что выбираем СУБД <strong>MySQL</strong>, а адрес сервера указываем <strong>db</strong>. По факту мы указываем не IP-адрес, а доменное имя. Явно мы его нигде не прописывали, но когда мы перечисляем сервисы в файле, Docker Compose присваивает эти имена адресам и внутри сети Docker все они прекрасно определяются. Так устроена эта контейнеризация.</p>
<p>Как только все будет готово и откроется уже админский аккаунт, браузер закрываем и в консоле на удаленном сервер нажимаем комбинацию <strong>Ctrl+C</strong>. Ждем пока все контейнеры остановятся.</p>
<h2 id="демонизация">Демонизация</h2>
<p>В таком состоянии облако будет работать пока будет открыта консоль сервера. Но ведь хочется, чтобы он работал постоянно. И эту задачу решить достаточно просто.</p>
<p>Для начала создаем файл командой <strong>nano /etc/systemd/system/nextcloud.service</strong> и записываем в него следующее:</p>
<pre tabindex="0"><code>
[Unit]
Description=Nextcloud docker-compose
Requires=docker.service
After=docker.service

[Service]
Restart=always

WorkingDirectory=/opt/nextcloud/

# Compose up
ExecStart=/usr/bin/docker-compose -f docker-compose.yml up

# Compose down, remove containers
ExecStop=/usr/bin/docker-compose -f docker-compose.yml down

[Install]
WantedBy=multi-user.target
</code></pre><p>Описание сервиса готово. Далее нужно сказать демону демонов (ну так получается), чтобы он обновил свой список демонов:</p>
<pre tabindex="0"><code>
systemctl daemon-reload
</code></pre><p>После включаем и запускаем:</p>
<pre tabindex="0"><code>
systemctl enable nextcloud
systemctl start nextcloud
</code></pre><p>Посмотреть что демон запущен:</p>
<pre tabindex="0"><code>
systemctl status nextcloud
</code></pre><p>Немного ждем и все готово. Можно пользоваться.</p>
<h2 id="обслуживание-системы--хорошая-система">Обслуживание системы = хорошая система</h2>
<p>В комплекте Nextcloud есть специальный файл, который нужно периодически запускать. Этот файл содержит в себе программный код обслуживания: удаление старых записей, очистка кэша, освобождения места и т.д. Этот файл может работать в 3-х режимах: при каждом запросе на сервер, периодический запрос к этому файлу удаленно, добавление его в планировщик. Я сначала пользовался вторым вариантом, но потом решил перенастроить его именно на локальный планировщик. Суть дела не меняет, но локальный запуск, я считаю, будет более правильным.</p>
<p>Для начала зайдем под паролем администратора в само облако и перейдем в Настройки-&gt;Основные параметры. У меня стояло <strong>Webcron</strong>, так как у меня было настроено так. Теперь я поставил <strong>Cron</strong>.</p>
<p>После этой манипуляции нужно настроить сам планировщик. Для начала нужно проверить, что все работает правильно. В консоле выполняем:</p>
<pre tabindex="0"><code>
docker ps
</code></pre><p>Находим в списке наш запущенный контейнер с сервером. В столбце в моем случае есть строчка <strong>nextcloud_nextcloud_1</strong>. Такое имя формирует Docker Compose. Если перезапустить службу, то будут созданы новые контейнеры, но Docker Compose присвоит эти же имена. Можно присвоить свои имена. Об этом ищите в документации к Docker и Docker Compose, а меня этот вариант более чем устраивает. По этому имени можно обращаться к запущенному контейнеру. Что нам и нужно.</p>
<p>И Попробуем выполнить:</p>
<pre tabindex="0"><code>
docker exec -it -u www-data nextcloud_nextcloud_1 php cron.php
</code></pre><p>Если все прошло успешно, то нужно добавить все это чудо в планировщик. Выполняем <strong>crontab -e</strong> и добавляем строчку:</p>
<pre tabindex="0"><code>
5 3 * * * docker exec -u www-data nextcloud_nextcloud_1 php cron.php
</code></pre><p>Здесь мы говорим планировщику, что запуска задание в 3 часа ночи 5 минут, каждый день.</p>
<p>Закрываем и сохраняем. Готово!</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-11-1024x65.png" alt="Работа контейнеров"></p>
<h2 id="что-получилось">Что получилось?</h2>
<p>В админском пользователе можно установить дополнения по вкусу. Я НАСТОЯТЕЛЬНО РЕКОМЕНДУЮ создать обычного пользователя и работать под ним, а админский пользователь останется для настройки и обслуживания.</p>
<p>Ко всему прочему можно «делиться» файлами и папками между облаками назначая различные права доступа. Так же можно зайти на сайт <a href="https://scan.nextcloud.com">https://scan.nextcloud.com</a> и проверить безопасность, но это можно будет сделать, если у вас есть внешний адрес, доменное имя и облако настроено через него по протоколу HTTPS. А вот об этом я расскажу немного позже. Дело не сложное, но очень полезное.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-10-1024x509.png" alt="В данном случае я просто не обновился до последней версии">Результат тестирования</p>
]]></content>
        </item>
        
        <item>
            <title>Разделение ресурсов и виртуализация</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D1%80%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%BE%D0%B2-%D0%B8-%D0%B2%D0%B8%D1%80%D1%82%D1%83%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F/</link>
            <pubDate>Mon, 06 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D1%80%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5-%D1%80%D0%B5%D1%81%D1%83%D1%80%D1%81%D0%BE%D0%B2-%D0%B8-%D0%B2%D0%B8%D1%80%D1%82%D1%83%D0%B0%D0%BB%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F/</guid>
            <description>&lt;p&gt;На сегодняшний день разделение ресурсов и виртуализация имеет огромное значение в промышленности. Существует огромное количество методов разделения вычислительных ресурсов. Я не буду углубляться в каждую технологию, а только постараюсь рассказать об основных  видах и принципах. Статья будет скорее посвящена для тех «кто в танке». Те кто уже знаком с этим и уж тем более использует в своей деятельно не найдут ничего нового. Попробуем…&lt;/p&gt;
&lt;h2 id=&#34;чистое-железо-bare-metal&#34;&gt;Чистое железо (Bare metal)&lt;/h2&gt;
&lt;p&gt;В данном разделе нет никакого принципа разделения ресурсов. Все используется по классике: есть компьютер, на него установлена операционная система, далее ставятся какие-то приложение. И на этом все. Каждое приложение выполняет свою задачу, ОС выдает какие-то ресурсы этим приложения. В данном случае требуется «голая» система, на которую уже устанавливается все остальное.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>На сегодняшний день разделение ресурсов и виртуализация имеет огромное значение в промышленности. Существует огромное количество методов разделения вычислительных ресурсов. Я не буду углубляться в каждую технологию, а только постараюсь рассказать об основных  видах и принципах. Статья будет скорее посвящена для тех «кто в танке». Те кто уже знаком с этим и уж тем более использует в своей деятельно не найдут ничего нового. Попробуем…</p>
<h2 id="чистое-железо-bare-metal">Чистое железо (Bare metal)</h2>
<p>В данном разделе нет никакого принципа разделения ресурсов. Все используется по классике: есть компьютер, на него установлена операционная система, далее ставятся какие-то приложение. И на этом все. Каждое приложение выполняет свою задачу, ОС выдает какие-то ресурсы этим приложения. В данном случае требуется «голая» система, на которую уже устанавливается все остальное.</p>
<p>Давайте приведем немного аналогии. Представим себе, что физический компьютер — это большой диван. Если взять несколько таких компьютеров, то это будет несколько диванов.</p>
<h2 id="виртуализация">Виртуализация</h2>
<p>Вот тут начинается более интересное движение. Смысл в том, что создается виртуальный компьютер, на который устанавливается полноценная ОС со своим ядром и приложениями. В данном случае на одном физическом компьютере можно создать несколько виртуальных компьютеров и они будут работать совместно. Конечно нужно учитывать количество ресурсов чтобы их хватило, но это дает возможность «нарезать» большое количество ресурсов на несколько меньшее количество. За счет такой схемы можно установить совершенно разные операционные системы на один компьютер и они будут работать параллельно. Это называется <a href="https://ru.wikipedia.org/wiki/%D0%93%D0%B8%D0%BF%D0%B5%D1%80%D0%B2%D0%B8%D0%B7%D0%BE%D1%80">Гипервизор</a>.</p>
<p>Стоит отметить поддержку такой виртуализации аппаратным обеспечением. Если «железо» не имеет такой поддержки, то сделать такую установку можно, но работать это будет крайне медленно. Очень многие современные процессоры умеют так делать.</p>
<p>Чтобы понять как это работает давайте посадим людей на этот диван. Люди бывают разные: толстые, худые, высокие, маленькие… В зависимости какой человек имеет комплекцию, столько он займет место на диване. Таким образом виртуальные машины могут иметь разное количество ресурсов в своем распоряжении.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-6-1024x573.png" alt="Пример работы виртуальной машины на сервере.">Виртуальная машина на сервере</p>
<h2 id="контейнеризация">Контейнеризация</h2>
<p>В случае <a href="https://ru.wikipedia.org/wiki/%D0%9A%D0%BE%D0%BD%D1%82%D0%B5%D0%B9%D0%BD%D0%B5%D1%80%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F">контейнеров</a> все работает немного иначе. Здесь нет никаких гипервизоров, аппаратной поддержки железа или чего-то еще. Все работает прямо в операционной системе. Но эта операционная система должна поддерживать такой вид изоляции. Да, в данном случае это уже изоляция, но она очень удобна в том, что можно нарезать ресурсы более тонко между приложениями и системами, при этом каждое приложение может получить в свое распоряжение свой набор настроект и утилит. Например, хостовая ОС может быть Debian, а внутри контейнера работать CentOS или Alpine.</p>
<p>По сути конейнерные ОС не имеют ядра, а пользуются ядром хостовой ОС. По этому контейнеры являются практически такими же ОС, что и главная, на которую это все устанавливается. Для такого типа разделения ресурсов не обязательно иметь аппаратную поддержку виртуализации. Все и так работает.</p>
<p>Попробуем представить как это выглядит. У нас есть все тот же диван, на котором сидят разные люди. Контейнер будем представлять в виде коробки, в которой что-то есть. Эти коробки бывают разного размера. Если дать эти коробки людям, то кто-то может взять их больше, а кто-то меньше. Но есть один момент — коробки можно поставить прямо на диван. В таком случае контейнеры будут работать прямо на голом железе. И так делать можно! Все зависит от задачи.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-8-1024x143.png" alt="Пример работы запущенных контейнеров">Пример запущенных контейнеров</p>
<h2 id="изоляция">Изоляция</h2>
<p>С точки зрения использования практически ничем не отличается от Контейнеризации, но все же определленные отличия имеются. Так изолированные приложения работают на той же ОС, что и хост. Так если хостовоая ОС является Ubuntu, то и приложение будет работать на Ubuntu. Просто приложение, как бы, «<a href="https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D1%81%D1%82%D1%80%D0%B0%D0%BD%D1%81%D1%82%D0%B2%D0%BE_%D0%B8%D0%BC%D1%91%D0%BD_%28Linux%29">изолируется</a>» от других и взаимодействие с «внешним миром» устанавливается какими-то правилами.</p>
<p>Чтобы представить как это работает, то представим себе пакетик, а в него мы положим что-то. Это и будет изоляция. Соответственно можно положить на диван, а можно раздать людям. Можно положить и в коробку, но такое редко практикуется.</p>
<h2 id="а-для-чего-все-это-нужно">А для чего все это нужно?</h2>
<p>Очень резонный вопрос. Суть в том, что бывают задачи, которые нужно изолировать друг от друга. Например, у Вас есть блог, сайт, интернет-магазин или вы просто хотите попробовать другую ОС. Может даже провести какие-то эксперименты, которые потом не нужны и их можно удалить. Бывают программы, которые требуют определенного рабочего окружения и оно не совместимо с текущим или другими программами. Может Вам нужно просто изолировать свои сервисы друг от друга. Например, вы строите <a href="https://ymnuktech.ru/home_server_hardware/">домашний сервер</a> и хотели бы сервисы изолировать друг от друга чтобы ими было удобно управлять. На самом деле причин может быть очень много, так что в данной теме есть из чего выбрать.</p>
<h2 id="послесловие">Послесловие</h2>
<p>Я хотел объяснить достаточно сложную тему простыми словами на сколько это возможно. На самом деле эта тема очень сложная, так как есть очень много подводных камней. Надеюсь я смог дать понять основной смысл всего этого «безобразия»…</p>
]]></content>
        </item>
        
        <item>
            <title>Резервное копирование (домашний сервер)</title>
            <link>https://ymnuktech.ru/posts/2021/12/%D1%80%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%B5-%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</link>
            <pubDate>Thu, 02 Dec 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/12/%D1%80%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%B5-%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80/</guid>
            <description>&lt;p&gt;Я уже писал про &lt;a href=&#34;https://ymnuktech.ru/about-backup&#34;&gt;резервное копирование&lt;/a&gt; и для чего это нужно. Я не буду повторяться в терминологии, так как в этом нет никакого смысла. Теперь наша задача это все дело настроить. Погнали!&lt;/p&gt;
&lt;h2 id=&#34;подготовка&#34;&gt;Подготовка&lt;/h2&gt;
&lt;p&gt;Прикупил я второй жесткий диск размером 2.5 дюйма для сервера и подключил его уже обычным переходником USB-&amp;gt;SATA. Как его подключить можно почитать «&lt;a href=&#34;https://ymnuktech.ru/home-server-os&#34;&gt;Домашний сервер (операционная система)&lt;/a&gt;«. Сажу только подключен он у меня по пути &lt;strong&gt;/mnt/backup&lt;/strong&gt;. Для резервного копирования буду использовать программу &lt;a href=&#34;https://borgbackup.readthedocs.io/en/stable/&#34;&gt;BorgBackup&lt;/a&gt;. На самом деле это форк программы &lt;a href=&#34;https://github.com/jborg/attic&#34;&gt;Attic&lt;/a&gt;. Собственно мне лично эта система понравилась своей простотой и достаточной эффективностью.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про <a href="https://ymnuktech.ru/about-backup">резервное копирование</a> и для чего это нужно. Я не буду повторяться в терминологии, так как в этом нет никакого смысла. Теперь наша задача это все дело настроить. Погнали!</p>
<h2 id="подготовка">Подготовка</h2>
<p>Прикупил я второй жесткий диск размером 2.5 дюйма для сервера и подключил его уже обычным переходником USB-&gt;SATA. Как его подключить можно почитать «<a href="https://ymnuktech.ru/home-server-os">Домашний сервер (операционная система)</a>«. Сажу только подключен он у меня по пути <strong>/mnt/backup</strong>. Для резервного копирования буду использовать программу <a href="https://borgbackup.readthedocs.io/en/stable/">BorgBackup</a>. На самом деле это форк программы <a href="https://github.com/jborg/attic">Attic</a>. Собственно мне лично эта система понравилась своей простотой и достаточной эффективностью.</p>
<p>Для начала установим ее:</p>
<pre tabindex="0"><code>
apt install -y borgbackup
</code></pre><p>Теперь нужно создать репозиторий для хранения данных:</p>
<pre tabindex="0"><code>
borg init -e none /mnt/backup/server
</code></pre><p>Здесь я создаю новый репозиторий без шифрования. На самом деле в промышленной эксплуатации этого делать не стоит, так как данные могу слить, но у меня домашний сервер и изолирован от внешнего мира, по этому я не стал заморачиватся. Для более подробной информации стоит почитать документацию и статью на хабре, где об этом прекрасно написано.</p>
<p>Теперь нужно создать скрипт для выполнения копирования. Создаем его и заполняем его следующим содержимым:</p>
<pre tabindex="0"><code>
#!/bin/bash
/usr/bin/borg create --stats /mnt/backup/server/cloud::&#34;cloud--{now:%Y-%m-%d_%H:%M:%S}&#34; /mnt/storage
/usr/bin/borg prune --keep-last=4 /mnt/backup/server
</code></pre><p>Первая строка создает новую резервную копию, а вторая очищает все предыдущие копии и оставляет только последние 4 штуки. Это нужно чтобы резервные копии перерабатывались и не забивали хранилище. Если же требуется настроить другое поведение, то это можно почитать в документации. Там много интересных функций.</p>
<p>Далее нужно сделать скрипт исполняемым:</p>
<pre tabindex="0"><code>
chmod +x backup.sh
</code></pre><p>И добавить это в планировщик. Планировщик редактируется так:</p>
<pre tabindex="0"><code>
crontab -e
</code></pre><p>И добавляется следующая строка:</p>
<pre tabindex="0"><code>
30 0 * * 6
</code></pre><p>Как пользоваться планировщиком написано <a href="https://ru.wikipedia.org/wiki/Cron">здесь</a>.</p>
<h2 id="восстановление">Восстановление</h2>
<p>Чтобы восстановить файлы из резервной копии я делаю монтирование нужной мне копии в директорию по просто пользуюсь копированием. Например:</p>
<pre tabindex="0"><code>
borgfs ::
</code></pre><h2 id="и-это-всё">И это всё?</h2>
<p>Да, это всё. Данный этап можно считать законченным, так как теперь по расписанию планировщика будет выполняться задание, которое и будет выполнять резервное копирование наших файлов. Так как BorgBackup сжимает данные да еще имеет функцию дедубликации. Эффективность хранения архивных копий прям возрастает.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-3-1024x221.png" alt="Общая информация о резервных копиях"></p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-4-1024x76.png" alt="Список копий"></p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-5-1024x272.png" alt="Информация о последней копии"></p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер (сетевые диски)</title>
            <link>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B5%D1%82%D0%B5%D0%B2%D1%8B%D0%B5-%D0%B4%D0%B8%D1%81%D0%BA%D0%B8/</link>
            <pubDate>Mon, 29 Nov 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D1%81%D0%B5%D1%82%D0%B5%D0%B2%D1%8B%D0%B5-%D0%B4%D0%B8%D1%81%D0%BA%D0%B8/</guid>
            <description>&lt;p&gt;Я уже писал про домашний сервер &lt;a href=&#34;https://ymnuktech.ru/home_server_hardware/&#34;&gt;здесь&lt;/a&gt; и &lt;a href=&#34;https://ymnuktech.ru/home-server-os&#34;&gt;здесь&lt;/a&gt;. Теперь нужно  сделать так, чтобы остальные могли подключаться и копировать файлы. А сделаем мы с помощью пакета &lt;a href=&#34;https://ru.wikipedia.org/wiki/Samba&#34;&gt;Samba&lt;/a&gt;. Для этого необходимо настроить сетевой диск для доступа и обмена. Приступим!&lt;/p&gt;
&lt;h2 id=&#34;диск-для-всех&#34;&gt;Диск для всех&lt;/h2&gt;
&lt;p&gt;И так, для начала необходимо установить некоторые программы. Что ж, сделаем:&lt;/p&gt;
&lt;p&gt;apt install -y samba&lt;/p&gt;
&lt;p&gt;Этого достаточно чтобы установить сервер, но не достаточно чтобы он заработал. Теперь нужно настроить программу, чтобы она отдавала нам файлы по сети. Для этого выполняем &lt;strong&gt;nano /etc/samba/smb.conf&lt;/strong&gt; и приводим конфигурационный файл примерно к следующему виду:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Я уже писал про домашний сервер <a href="https://ymnuktech.ru/home_server_hardware/">здесь</a> и <a href="https://ymnuktech.ru/home-server-os">здесь</a>. Теперь нужно  сделать так, чтобы остальные могли подключаться и копировать файлы. А сделаем мы с помощью пакета <a href="https://ru.wikipedia.org/wiki/Samba">Samba</a>. Для этого необходимо настроить сетевой диск для доступа и обмена. Приступим!</p>
<h2 id="диск-для-всех">Диск для всех</h2>
<p>И так, для начала необходимо установить некоторые программы. Что ж, сделаем:</p>
<p>apt install -y samba</p>
<p>Этого достаточно чтобы установить сервер, но не достаточно чтобы он заработал. Теперь нужно настроить программу, чтобы она отдавала нам файлы по сети. Для этого выполняем <strong>nano /etc/samba/smb.conf</strong> и приводим конфигурационный файл примерно к следующему виду:</p>
<pre tabindex="0"><code>
[global]
   workgroup = MYNAS
   dns proxy = no

   log file = /var/log/samba/log.%m

   max log size = 1000

   syslog = 0

   panic action = /usr/share/samba/panic-action %d

   server role = standalone server

   passdb backend = tdbsam

   obey pam restrictions = yes

   unix password sync = yes

   passwd program = /usr/bin/passwd %u
   passwd chat = *Enter\snew\s*\spassword:* %n\n *Retype\snew\s*\spassword:* %n\n *password\supdated\ssuccessfully* .

   pam password change = yes

/bin/false %u

   usershare allow guests = yes

   ntlm auth = yes

[storage]
   comment = Share NAS
   browsable = yes
   path = /mnt/storage
   guest ok = yes
   read only = no
   create mask = 0777
   directory mask = 0777
   writeable = yes
   valid users = user
   locking = no
   oplocks = yes
   strict locking = no
   share modes = yes
</code></pre><p>Здесь я привожу свой пример без комментариев. Данный пример не является истинно верным и не претендует на уникальность. Все что Вам нужно изменить и донастроить Вы можете это сделать. А мы идем дальше…</p>
<p>Теперь нам нужен пользователь, с помощью которого мы буем подключаться к сетевому диску. Для этого создаем его в Samba:</p>
<pre tabindex="0"><code>
smbpasswd -a user
</code></pre><p>Перед применением всего этого добра сервер, вероятнее всего, будет ругаться на количество открываемых файлов. Это связано в ограничении. Добавим пару срочек в <strong>/etc/security/limits.conf</strong>:</p>
<pre tabindex="0"><code>
*               hard    nofile          1048576
*               soft    nofile          1048576
</code></pre><p>Теперь проверим все ли хорошо:</p>
<pre tabindex="0"><code>
testparm
</code></pre><p>И, если все хорошо, перезапустим службу:</p>
<pre tabindex="0"><code>
systemctl stop smbd &amp;&amp; systemctl restart nmbd &amp;&amp; systemctl start smbd
systemctl enable smbd &amp;&amp; systemctl enable nmbd
</code></pre><p>Теперь можно подключаться с Windows-компьютера указав адрес \\storage и ввести логин и пароль пользователя, которого Вы указали.</p>
<h2 id="клиент-на-linux">Клиент на Linux</h2>
<p>Тут ничего сложного нет. Для этого создаем директорию:</p>
<pre tabindex="0"><code>
mkdir -p /mnt/storage
</code></pre><p>Установим необходимые пакеты:</p>
<pre tabindex="0"><code>
apt install -y cifs-utils
</code></pre><p>И в <strong>/etc/fstab</strong> добавим одну строчку:</p>
<pre tabindex="0"><code>
///storage /mnt/storage    cifs    credentials=/etc/home_store.cred,file_mode=0777,dir_mode=0777,rw        0 0
</code></pre><p>Затем создаем файл <strong>/etc/home_store.cred</strong>:</p>
<pre tabindex="0"><code>
username=
password=
</code></pre><p>И устанавливаем права доступа:</p>
<pre tabindex="0"><code>
sudo chmod 600 /etc/home_store.cred
</code></pre><p>Теперь все монтируем:</p>
<pre tabindex="0"><code>
sudo mount -a
</code></pre><p>В принципе все.</p>
<h2 id="еще-один-сетевой-диск">Еще один сетевой диск</h2>
<p>Теперь нам необходимо настроить <a href="https://ru.wikipedia.org/wiki/Network_File_System">NFS</a>. Это самая родная сетевая файловая система для Linux. Так как у меня есть второй одноплатник и я на него собираюсь ставить различные приложения, то мне нужно где-то хранить данные. Буду их хранить на сервере с файлами. Можно было бы использовать ту же <strong>Samba</strong>, но есть некоторые проблемы и ограничения которые отчасти сложно обходить, да и зачем мучиться, если можно сделать все гораздо проще. Продолжим…</p>
<p>Для начала установим все необходимые пакеты на сервер хранилища и сразу его запустим:</p>
<pre tabindex="0"><code>
apt install -y nfs-common nfs-kernel-server
systemctl start nfs-server &amp;&amp; systemctl enable nfs-server
</code></pre><p>Так как у меня уже есть расшаренная сетевая директория для <strong>Samba</strong> и я не хочу придумывать другую, то буду использовать ее же. Далее нужно сказать NFS-серверу что мы хотим отдать другим. Для этого редактируем файл <strong>/etc/exports</strong> (я просто добавил строчку):</p>
<pre tabindex="0"><code>
/mnt/storage    (rw,no_root_squash)
</code></pre><p>И просто перезапускаем сервер:</p>
<pre tabindex="0"><code>
systemctl restart nfs-server
</code></pre><h3 id="клиент-другого-сетевого-диска">Клиент другого сетевого диска</h3>
<p>На будущем сервер устанавливаем пакет клиента:</p>
<pre tabindex="0"><code>
apt install -н nfs-common
</code></pre><p>в <strong>/etc/fstab</strong> добавляем строчку:</p>
<pre tabindex="0"><code>
:/mnt/storage/     /mnt/nfs/       nfs     rw      0 1
</code></pre><p>Создаем директорию, к которой будем подключаться:</p>
<pre tabindex="0"><code>
mkdir -p /mnt/nfs
</code></pre><p>И монтируем:</p>
<pre tabindex="0"><code>
mount -a
</code></pre><p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-2-1024x306.png" alt="Подключенные сетевые диски"></p>
<h2 id="как-бы-всё">Как бы всё!</h2>
<p>Собственно на этом все. Основа готова. Теперь уже можно что-то дальше наворачивать, так как есть и сервер для хранения данных и сервер для обработки каких-либо данных. Да, это не самолет с реактивным двигателем, но какие-то домашние задачи вполне можно решать.</p>
]]></content>
        </item>
        
        <item>
            <title>О резервном копировании</title>
            <link>https://ymnuktech.ru/posts/2021/11/%D0%BE-%D1%80%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B8/</link>
            <pubDate>Thu, 25 Nov 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/11/%D0%BE-%D1%80%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%BC-%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B8/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://ymnuktech.ru/images/posts/backup_example.png&#34; alt=&#34;Пример резервного копирования&#34;&gt;&lt;/p&gt;
&lt;p&gt;Данный пост я хочу посвятить рассуждению что это такое и зачем оно нужно. Мне достаточно часто приходилось сталкиваться с отсутствием резервных копий файлов, которые так нужны и исчезают в самый неподходящий момент. Может хоть немного люди будут задумываться о резервном копировании…&lt;/p&gt;
&lt;h2 id=&#34;что-это-такое&#34;&gt;Что это такое?&lt;/h2&gt;
&lt;p&gt;И так, что такое резервное копирование? На этот вопрос отвечали уже очень много раз на разных ресурсах интернета, в поисковике находится по щелчку пальцев, но люди часто этим пренебрегают, а уж техническим специалистам должно быть позор такого не знать! Попробуем это в очередной раз исправить.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p><img src="/images/posts/backup_example.png" alt="Пример резервного копирования"></p>
<p>Данный пост я хочу посвятить рассуждению что это такое и зачем оно нужно. Мне достаточно часто приходилось сталкиваться с отсутствием резервных копий файлов, которые так нужны и исчезают в самый неподходящий момент. Может хоть немного люди будут задумываться о резервном копировании…</p>
<h2 id="что-это-такое">Что это такое?</h2>
<p>И так, что такое резервное копирование? На этот вопрос отвечали уже очень много раз на разных ресурсах интернета, в поисковике находится по щелчку пальцев, но люди часто этим пренебрегают, а уж техническим специалистам должно быть позор такого не знать! Попробуем это в очередной раз исправить.</p>
<p><a href="https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%B7%D0%B5%D1%80%D0%B2%D0%BD%D0%BE%D0%B5_%D0%BA%D0%BE%D0%BF%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">Резервное копирование</a> — это процесс копирования важных и необходимых данных в совершенно другое место с целью защитить их от утере и/или порчи. Больше по определению добавить нечего…</p>
<h2 id="как-происходит-процесс">Как происходит процесс?</h2>
<p>В этом процессе ничего нового не происходит. По сути выполняется копирование данных вручную либо автоматически. Мы просто копируем данные в какое-то хранилище. Это может быть другая директория, диск или что-то более специализированное, т.е. на другой носитель.</p>
<h2 id="как-это-можно-делать">Как это можно делать?</h2>
<p>На сегодняшний день способов существует очень много и каждый может выбрать то, что подходит по его конкретные задачи. Можно сделать что-то свое маленькое, а можно стрелять «из пушки по воробьям» используя целые комплексы.</p>
<h3 id="ручное-копирование">Ручное копирование</h3>
<p>В этом способе нет ничего сложного. Просто выделяем файлы и директории и копируем в нужное нам место. Ничего интересного.</p>
<h3 id="автоматизация-скриптами">Автоматизация скриптами</h3>
<p>Для начала нужно определиться что такое скрипт.</p>
<p>Скрипт — это набор команд, которые объединены в один файл и выполняются последовательно. По другому называется <a href="https://ru.wikipedia.org/wiki/%D0%A1%D1%86%D0%B5%D0%BD%D0%B0%D1%80%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA">сценарный язык</a>. В разных операционных системах может выглядеть по разному, но принцип действия у них практически одинаковый.</p>
<p>Для Windows это могут быть командные файлы bat или cmd, которые выполняет командный процессор cmd.exe. Так же может использоваться VBS или JScript со своим процессом выполнения cscript. Так же может быть использован PowerShell.</p>
<p>Для Linux очень частая практика использования bash-скриптов. Так же могут использоваться Perl и Python. Это уже, по сути, интерпретируем языки программирования, используемые для более сложной и развернутой автоматизации. На них так же пишут сложные целые программы и даже программные комплексы.</p>
<h3 id="специализированное-по">Специализированное ПО</h3>
<p>Если не хочется  писать такие сценарии самостоятельно, то рынок предлагает целые готовые решения от простых бесплатных вариантов до крупных сложных систем. Вот тут можно уже выбирать что нравится и что подходит под конкретные задачи. Скажу только, что не нужно для домашнего ПК выбирать программу уровня Enterprise. Оно не окупится на его поддержку.</p>
<h2 id="куда-сохранять">Куда сохранять?</h2>
<p>Тоже не маловажный вопрос. Если у Вас не много данных, то достаточно будет флешки. Если же данных достаточно много, то, вероятно, стоит задуматься о съемном жестком диске. Тут все просто: подключаем, копируем, отключаем.</p>
<p>Еще можно копировать данные в «Облако». Этот способ хорош тем, что к данным можно получить доступ без физического носителя, т.е. вы просто их скачиваете.</p>
<p>Есть более сложный вариант для домашнего использования: <a href="https://ymnuktech.ru/home_server_hardware/">домашний сервер</a>. Я для себя выбрал такой вариант. Каждый может выбрать то что требуется.</p>
<p>Для уровня предприятия используются более сложные сценарии. Тут уже идет в ход все начиная от обычных скриптов и заканчивая целыми серверами резервного копирования. Технологий используется масса. Так скриптами может производиться подготовка данных и уже потом подготовленные данные сохраняться на носители. Кстати носителями информации могут быть и <a href="https://ru.wikipedia.org/wiki/LTO">магнитные ленты</a>.</p>
<h2 id="а-как-же-пример">А как же пример?</h2>
<p>Я не буду придумывать что-то сложное а просто покажу пример в Linux:</p>
<p>tar cjf .tar.bz2</p>
<p>Данная команда создаст архив tar и сожмет его архиватором bz2. Это и будет резервная копия. Останется написать сценарий и добавить его в планировщик задач.</p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер (операционная система)</title>
            <link>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0/</link>
            <pubDate>Tue, 23 Nov 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%BE%D0%BF%D0%B5%D1%80%D0%B0%D1%86%D0%B8%D0%BE%D0%BD%D0%BD%D0%B0%D1%8F-%D1%81%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%B0/</guid>
            <description>&lt;p&gt;И так. Железо у нас есть, в кучу это собрано, операционная система не установлена. Хм… Упущение… Значит нужно исправлять. А то что я для себя выбирал, то можно посмотреть в статье &lt;a href=&#34;https://ymnuktech.ru/home_server_hardware/&#34;&gt;Домашний сервер (железо)&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Ну что же. Приступим!&lt;/p&gt;
&lt;h2 id=&#34;операционная-система&#34;&gt;Операционная система&lt;/h2&gt;
&lt;p&gt;Как я уже писал ранее, выбрал для себя &lt;a href=&#34;http://www.armbian.com&#34;&gt;Armbian&lt;/a&gt;, а конкретно образы для &lt;a href=&#34;https://www.armbian.com/orange-pi-prime/&#34;&gt;Orange Pi Prime&lt;/a&gt; и &lt;a href=&#34;https://www.armbian.com/orange-pi-zero-plus/&#34;&gt;Orange Pi Zero Plus&lt;/a&gt;. Установка достаточно простая. Для начала нужно распаковать архивы, а затем на каждую флэшку записать образ. Вот как это делается:&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>И так. Железо у нас есть, в кучу это собрано, операционная система не установлена. Хм… Упущение… Значит нужно исправлять. А то что я для себя выбирал, то можно посмотреть в статье <a href="https://ymnuktech.ru/home_server_hardware/">Домашний сервер (железо)</a>.</p>
<p>Ну что же. Приступим!</p>
<h2 id="операционная-система">Операционная система</h2>
<p>Как я уже писал ранее, выбрал для себя <a href="http://www.armbian.com">Armbian</a>, а конкретно образы для <a href="https://www.armbian.com/orange-pi-prime/">Orange Pi Prime</a> и <a href="https://www.armbian.com/orange-pi-zero-plus/">Orange Pi Zero Plus</a>. Установка достаточно простая. Для начала нужно распаковать архивы, а затем на каждую флэшку записать образ. Вот как это делается:</p>
<pre tabindex="0"><code>
dd if= of=/dev/sd bs=1M status=progress
</code></pre><p>Если не в курсе какая буква, то не беда. Вбиваем в консоль команду:</p>
<pre tabindex="0"><code>
lsblk
</code></pre><p>Готово! Теперь собираем все в кучу, подключаем монитор и запускаем.</p>
<p>Для Orange Pi Zero Plus подключаем UART и вставляем в USB. Далее я делал через программу screen.</p>
<pre tabindex="0"><code>
screen /dev/ttyUSB0
</code></pre><p>Кстати, путь может отличаться до USB-устройства, так что следует проверить. После просто отвечаем на вопросы.</p>
<h2 id="настройка-сети">Настройка сети</h2>
<p>Когда у нас все готово нужно настроить сеть. Я отказался от NetworkManager (за ненадобностью). Отключаем его (все эти и дальнейшие команды выполняю от суперпользователя root):</p>
<pre tabindex="0"><code>
systemctl stop NetworkManager &amp;&amp; systemctl disable NetworkManager
</code></pre><p>Нужно еще проверить имя сетевого интерфейса. Для этого вбиваем следующую команду:</p>
<pre tabindex="0"><code>
ip a
</code></pre><p>Вот мой результат:</p>
<pre tabindex="0"><code>
1: lo:  mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0:  mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether
    inet  brd  scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::1:1cff:fe0b:dab6/64 scope link
       valid_lft forever preferred_lft forever
3: wlan0:  mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether
</code></pre><p>Интерфейс wlan0 — это Wi-Fi. Он мне не нужен, по этому настраивать я его не буду. Ищем что-то типа <strong>eth</strong> или <strong>ens</strong>. Это и будет наш проводной интерфейс.</p>
<p>Теперь открываю файл на редактирование <strong>/etc/network/interfaces</strong> и привожу к следующему виду, соответственно указывая нужные мне адреса. Для обоих одноплатников должны быть разные адреса в поле <strong>address</strong> и эти адреса должны быть свободными, ну и должны находиться в диапазоне Вашей сети:</p>
<pre tabindex="0"><code>
# Network is managed by Network manager
auto lo
iface lo inet loopback

auto
iface eth0 inet static
address /
gateway
dns-nameservers
</code></pre><p>Далее мне лень вбивать команды перезапуска сети, по этому я просто перезагружаю:</p>
<pre tabindex="0"><code>
/usr/sbin/reboot
</code></pre><h3 id="если-захотелось-networkmanager">Если захотелось NetworkManager</h3>
<p>Если уж хочется настроить сеть через NetworkManager, то останавливать службу не нужно. Достаточно выполнить команду в консоли <strong>nmtui</strong> и выполнить настройку.</p>
<h2 id="подключение-дисков">Подключение дисков</h2>
<p>Сеть настроена, жесткий диск подключен, но он не используется. Значит надо подключать.</p>
<p>Так как у нас система загружается с SD-карты, то понадобится swap-раздел, а для хранения данных основной. Чтож готовим разделы.</p>
<p>Для всего этого добра воспользуемся программой разметки диска. Я не буду описывать как это делать, так как в интернете этой информации просто ВАЛОМ. Для выполнения такой работы можно использовать программы fdisk, gdisk и parted. Первая предназначена для MBR-разделов, вторая для GPT-разделов, а третья может работать в обоих режимах, так что выбираем что больше нравится. Единственное GPT-разделы предподчительней использовать, так они поддерживают больший объем, чем MBR. Об этом можно почитать в <a href="https://ru.wikipedia.org/wiki/%D0%A2%D0%B0%D0%B1%D0%BB%D0%B8%D1%86%D0%B0_%D1%80%D0%B0%D0%B7%D0%B4%D0%B5%D0%BB%D0%BE%D0%B2_GUID">Википедии.</a></p>
<p>Наша задача создать 2 раздела. Первый раздел располагаем в начале диска размером 4ГБ (более чем достаточно) для раздела SWAP, а второй будет все оставшееся пространство. Его форматируем в EXT4 (можно выбрать другую файловую систему по вкусу). Как создать SWAP и форматировать в EXT4 (или другие файловые системы ищем в интернете). Как работать с разделами прекрасно расписано, например, <a href="https://wiki.gentoo.org/wiki/Handbook:AMD64/Installation/Disks/ru">здесь</a>.</p>
<p>Далее необходимо создать директорию куда будем подключать наше хранилище. Для этого создаем директорию. У меня сделано так:</p>
<pre tabindex="0"><code>
mkdir -p /mnt/storage
</code></pre><p>Далее надо посмотрим какие диски имею метки UUID:</p>
<pre tabindex="0"><code>
blkid
</code></pre><p>После открываем файл fstab:</p>
<pre tabindex="0"><code>
nano /etc/fstab
</code></pre><p>И добавляем строчки для автомонтирования разделов при загрузке:</p>
<pre tabindex="0"><code>
UUID= none  swap    sw      0 0
UUID=&lt;&gt;UUID раздела для будущих файлов /mnt/storage ext4 defaults,noatime,commit=600,errors=remount-ro 0 1    defaults,noatime,commit=600,errors=remount-ro 0 1
</code></pre><p>Под конец выполняем монтирование:</p>
<pre tabindex="0"><code>
mount -a
</code></pre><p>Проверяем что все хорошо:</p>
<pre tabindex="0"><code>
mount
</code></pre><h2 id="вместо-заключения">Вместо заключения</h2>
<p>На текущий момент это была первоначальная настройка будущих серверов. По итогу я получил работающие компьютеры, с которыми можно продолжать работать. На данном этапе все это добро можно смело убирать в корпус и приводить в порядок все провода и убирать всю систему куда-нибудь на полку, где не будет все это добро мешаться.</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5.png" alt="Приветствие при входе">Рабочая система</p>
<p><img src="/images/posts/%D0%B8%D0%B7%D0%BE%D0%B1%D1%80%D0%B0%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5-1.png" alt="Присутствующие диски и разделы на домашнем сервере">Диски</p>
]]></content>
        </item>
        
        <item>
            <title>Домашний сервер (железо)</title>
            <link>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%B6%D0%B5%D0%BB%D0%B5%D0%B7%D0%BE/</link>
            <pubDate>Tue, 16 Nov 2021 00:00:00 +0000</pubDate>
            
            <guid>https://ymnuktech.ru/posts/2021/11/%D0%B4%D0%BE%D0%BC%D0%B0%D1%88%D0%BD%D0%B8%D0%B9-%D1%81%D0%B5%D1%80%D0%B2%D0%B5%D1%80-%D0%B6%D0%B5%D0%BB%D0%B5%D0%B7%D0%BE/</guid>
            <description>&lt;p&gt;Тема интересна для тех, кто хочет организовать домашний сервер, на котором можно хранить свои личные данные: фото, музыку, фильмы, документы и т.д. Ограничения касаются только объемом жесткого диска. Но можно еще и управлять домашней сетью, а для особых параноиков еще и организовать домашнюю защиту от проникновения. Тут уже кому что захочется.&lt;/p&gt;
&lt;p&gt;Вариант №1 — самый простой и банальный вариант, это пойти в магазин и купить NAS-сервер. Стоимость таких устройств самая разнообразная. Думаю каждый найдет для себя то что он хочет.&lt;/p&gt;</description>
            <content type="html"><![CDATA[<p>Тема интересна для тех, кто хочет организовать домашний сервер, на котором можно хранить свои личные данные: фото, музыку, фильмы, документы и т.д. Ограничения касаются только объемом жесткого диска. Но можно еще и управлять домашней сетью, а для особых параноиков еще и организовать домашнюю защиту от проникновения. Тут уже кому что захочется.</p>
<p>Вариант №1 — самый простой и банальный вариант, это пойти в магазин и купить NAS-сервер. Стоимость таких устройств самая разнообразная. Думаю каждый найдет для себя то что он хочет.</p>
<p>Вариант №2 может подойти тем, у кого есть дома в углу старое железо, которое выбросить жалко и можно найти рабочие устройства. С этого можно собрать рабочий системный блок и, при необходимости докупить жесткие диски.</p>
<p>Вариант №3 — вот с этого момента я постараюсь рассказать тему более малого размера сервера. А сервер будет строить на одноплатниках.</p>
<p>Данная статья не претендует на уникальность и описывает только мой личный опыт. И так — поехали!</p>
<h2 id="на-что-можно-посмотреть">На что можно посмотреть</h2>
<p>Первым делом нужно выбрать плату. Таких плат и производителей уж очень много на сегодняшний день. Тут есть практически на любой вкус и цвет процессоры. С размером ОЗУ тоже все достаточно понятно: от 512МБ и до 8ГБ (то что я видел на картинках).</p>
<p>Очень многие одноплатники имеют слот под SD-карту, но очень мало имеют SATA-разъемы. SD-карты нужны, чтобы установить первоначальную операционную систему и запустить ее уже в рабочем режиме. Некоторые из моделей имеют «на борту» у себя eMMC-микросхему, а которую можно «перенести» установленную ОС, тем самым избавиться от «флешки».</p>
<p>Что касается носителей (HDD/SDD), то в этом плане немного сложнее. Здесь можно использовать 3 варианта:</p>
<ul>
<li>Плату уже с распаянным SATA-разъемом;- Плату расширения с SATA-разъемом;- Переходник USB-&gt;SATA.</li>
</ul>
<p>Собственно разницы особо никакой, потому что даже первый вариант в большинстве случаев является переходником с USB на SATA, только уже сделанный для удобства.</p>
<p>Я в 2018 году долго выбирал что бы взять подешевле. В итоге остановился на Orange Pi Zero Plus с 512МБ. Такая себе вполне вменяемая плата, но на ней отсутствует SATA. Чтобы подключить диск нужна плата расширения либо переходник. Вот как раз у производителя есть такая плата расширения, но она все так же является переходником с USB.</p>
<p>Что касается ограничений, то это USB 2.0, так что от него ничего производительного ждать не стоит (если вы надеетесь на сотни мегабайт в секунду). В общем про SSD можно забыть. А что по скорости HDD, то я проверял последовательность чтения и записи и она не превышает 40МБ/с, так что жесткий диск тоже напрягаться не будет.</p>
<h2 id="электричество">Электричество</h2>
<p>Вот тут нас поджидает небольшой облом… Дело в том что у этого одноплатника есть питание через OTG, но куча всяких блоков питания не хочется подключать. Это придется удлинитель куда-то засовывать, да и вообще, не по фэншую это все. А так как я еще заказывал Orange Pi Prime на 2ГБ, то там уже не было питание через OTG, а присутствует разъем под блок питания. Кстати, для обеих плат рекомендован БП на 2А при 5В. А вот в NAS-расширении присутсвтует ровно такой же разъем.</p>
<p>Ну что же. Пришел час старого доброго БП от стационарника. Подключим его, ибо в нем хватает всего и вся. Мощность его всего 350Вт, но даже этого хватает с головой.</p>
<p>Пошел я в Китай-магазин и заказал 10шт разъемов питания 24pin под БП и решил еще прикупить разъемов USB пачкой (они так же пригодятся). Пришло, хорошо. Теперь нужно все это распаять. Так как у БП формата ATX питание включается только при определенных условиях, то нужно их выполнить. Вот распиновка разъема (ищется на раз):</p>
<p><img src="https://ic.pics.livejournal.com/elchupanibrei/30212434/52886/52886_800.jpg" alt="Ремонт SPI ATX-400PN aka FSP ATX-400PN: elchupanibrei — LiveJournal">24 pin</p>
<p>Чтобы БП заработал нужно замкнуть PS-ON и Ground обычной скрепкой. И вот тут нужно идти в радио-магазин за кнопкой с фиксацией. Заодно прикупить штекеров для компов.</p>
<p>Теперь берем разъем и припаиваем проводами штекеры к Ground и +5V. К ним же можно припаять USB-разъемы. Можно подключить телефончик для зарядки или еще какую-нибудь штуку, типа ионизатора воздуха. Найти применение можно.</p>
<p>После сборки подключаем, проверяем — работает!</p>
<h2 id="операционная-система">Операционная система</h2>
<p>Сразу скажу: про форточки можно забыть! Если взять практически любой роутер или тот же магазинный NAS-сервер, то там стоит Linux, так что при ручной настройки придется поковыряться. Я не буду здесь описывать готовые решения «поставил и играй». Здесь мы будем ставить все изнутри ручками.</p>
<p>Для начала нужно выбрать и установить какой-нибудь дистрибутив. Тут кому что нравится. Мне лично понравился <a href="https://www.armbian.com">Armbian</a> на основе Debian. Загружаем и устанавливаем. Не скажу как это сделать в Windows (не пользуюсь), но вот в Linux это достаточно просто. Распаковываем архив и выполняем команду:</p>
<p>dd of=/dev/sd if= bs=1M status=progress</p>
<p>Мне нравится видеть сколько скопировано, по этому и указан статус.</p>
<p>Ждем окончания и все готово.</p>
<p>Если есть выход HDMI, то подключаем монитор и настраиваем (статей в интернете полно). Если нету, то вот тут нужен USB-&gt;UAR, который тоже нужно заблаговременно приобрести. Подключать нужно Rx-&gt;Tx, Tx-&gt;Rx, GND-&gt;GND. Подключаем, запускаем, открываем последовательный терминал типа Telnet, Putty, Screen (Linux), подключаемся и точно так же делаем первоначальную настройку и настраиваем сеть.</p>
<h2 id="связь">Связь</h2>
<p>Так как это бездисплейные домашний серверы и мы хотим чтобы они работали по сети, то их нужно как-то подключить в домашнюю локальную сеть. У меня в закромах был Коммутатор 100Tx-Base. Для моих задач мне хватает, тем более на текущий момент у меня скорости чтения с диска не превышает 15МБ/с, а по сети предел 100МБит/с или примерно 10МБ/с. 5 портов на текущий момент хватает (2 платы, TV-Box и порт от роутера). Wi-Fi на платах я не использую.</p>
<h2 id="барахло-на-коленке">Барахло на коленке</h2>
<p>Как-то грустно это все валяется на полу. Я вижу 3 варианта:</p>
<ul>
<li>Пусть оно так и валяется пока кто-нибудь не убьется или пока домашние не вышвырнут из жилплощади;- Старый системный корпус;- Сделать свой корпус с шахматами и пивом.</li>
</ul>
<p>3-й вариант ну уж очень вкусно выглядит, так как можно придумать все что угодно, но… Да чего тут говорить, просто лень что-то придумывать. Тут либо из фанеры лобзиком вырезать, либо из металла/оргстекла/чего-то лепить, либо разработать корпус и напечатать. Кстати если есть 3D-принтер то это не должно быть проблемой, а если нет, то, наверно же, можно найти у кого он есть. Главное разработать проект печати.</p>
<p>В итоге я решил сделать из старого корпуса, а к 3D-печати может потом вернусь и реализую.</p>
<p>Для начала бежим в строительный магазин и покупаем пакеты винтов М3, стоечек того же формата и шайбы под все это (лишним прям совсем не будет). После все платы прикручиваем внутри корпуса с помощью молотка, так как расстояния отверстий на плате и на задней стенке под материнскую плату не совпадают (тут меня долго бомбит из-за 0,5мм разброса) и подключаем все провода.</p>
<p><img src="/images/posts/IMG_20211116_170911_cut-153x300.jpg" alt="Корпус"></p>
<p><img src="/images/posts/IMG_20211116_170931_cut-300x239.jpg" alt="Сервер хранения данных"></p>
<p><img src="/images/posts/IMG_20211116_170953_cut-300x170.jpg" alt="Сервер обработки">Сервер обработки</p>
<p><img src="/images/posts/IMG_20211116_171032_cut-300x190.jpg" alt="Разъем питания">Разъем питания</p>
<h2 id="итоги">Итоги</h2>
<p>Если посчитать по стоимости, то обошлась мне эта сборка около 10000 рублей вместе с диском на 1ТБ, 2-мя MicroSD на 16ГБ и двумя одноплатниками плюс рассыпуха по мелочи. По времени дня 4 с установкой ОС, настройкой сетевых дисков, отладкой работы и объяснением домочадцам как это работает и на кой оно надо.</p>
]]></content>
        </item>
        
    </channel>
</rss>
