Предыстория

Имелся у меня машрутизатор на Raspberry Pi4 и на таком-же работал Home Assistant. В какой-то момент понадобился дополнительный UpLink в маршрутезаторе, по что был задействован USB-2-Ethernet адаптер. И всё бы было хорошо, но на ядрах 5.15 и выше, USB сетевой адаптер начал сбрасываться под нагрузкой. Некоторое время повозившись в попытках решить проблему, пришёл к выводу, что пора перетянуть маршрутизатор на что-то с большим количеством и не тратить время на решение проблем USB на RPi. Раз уж решил перевозить маршрутизатор, то и Home Assistant стоило разместить на одном устройстве с маршрутизатором.

Железо

Поиски адекватных вариантов на ARM с количеством независимых Eth портов не увенчался успехом, поэтому остановился на покупке мини пк на Алиэкспрес, вот такой вариант:

  • CPU: Intel Celeron N5105

  • Ethernet: 5xi226V 2.5Gb

  • Memory: 16Gb

  • SSD: 256Gb

В общем, железо с большим запасом и вполне приемлемой ценой, мне обошлось чуть менее $250 вместе с доставкой.

Установка OpenWRT

С этим всё по уже имеющимся докам, ни каких заморочек не возникло. Использовал образ, собранный самостоятельно при помощи openwrt-imagebuilder-22.03.3-x86-64. Пересобирал, с целью получить требуемые компоненты непосредственно в образе и не заниматься доустановкой ручками. Скрипт для сборки со списком компонентов:

#!/bin/sh
# Update imagebuilder from https://downloads.openwrt.org/snapshots/targets/x86/64/

# Be shure, to save /root /home and additional config files from /etc


# rpi-eeprom-update -a  --- eeprom firmware update

if [ -d ../files ]; then
        [ -d ./files ] && rm -rf ./files
        cp -a ../files ./
fi

# Set CONFIG_TARGET_ROOTFS_PARTSIZE=4096 in .config file
KERNEL_PARTSIZE=32
ROOTFS_SIZE=4096
sed -i -re "s/^(CONFIG_TARGET_KERNEL_PARTSIZE=).*$/\\1$KERNEL_PARTSIZE/" .config
sed -i -re "s/^(CONFIG_TARGET_ROOTFS_PARTSIZE=).*$/\\1$ROOTFS_SIZE/" .config

sed -i -re "s/256/1024/" target/linux/x86/image/Makefile

PACKAGES='\
        -dnsmasq        \
\
        kmod-usb-net-rtl8152    kmod-ipt-nat            kmod-crypto-sha256      kmod-i2c-core           kmod-scsi-generic       kmod-scsi-core          \
        kmod-i2c-smbus          kmod-i2c-gpio           kmod-mii                kmod-usb-net            kmod-usb-wdm            kmod-usb-net-qmi-wwan   \
        kmod-usb-net-cdc-mbim   kmod-usb-serial-option  kmod-usb-serial         kmod-usb-serial-wwan    kmod-usb-net-cdc-ether  kmod-usb-storage        \
        kmod-usb-xhci-hcd       kmod-ipt-nat-extra      kmod-inet-diag          kmod-bonding            kmod-kvm-intel          kmod-usb-storage-uas    \
        kmod-ipt-checksum       \
\
        screen                  sudo                    dmesg                   htop                    mc              \
        usbutils                bash                    zsh                     owfs                    openvpn-wolfssl         openssh-server  \
        openssh-client          nmap                    tcpdump                 umbim                   ethtool-full            uhubctl         \
        bind-client             bind-host               bind-nslookup           bind-server             bind-tools              bind-dig        \
        udpxy                   zabbix-agentd           i2c-tools               gcc                     make                    ntpd            \
        nginx-ssl               bwm-ng                  nmap                    lynx                    usb-modeswitch          uqmi            \
        procps-ng-sysctl        procps-ng-watch         procps-ng-uptime        procps-ng-top           procps-ng-tload         procps-ng-ps    \
        python3                 python3-setuptools      fdisk                   iptables-mod-checksum   \
        lua                     luasocket               luasec                  luabitop                iconv                   idn             \
        dnsmasq-full            block-mount             agetty                  mwan3                   bmon                    \
        qemu-firmware-efi       qemu-x86_64-softmmu     qemu-bridge-helper      qemu-img                \
        python3-paho-mqtt       \
\
        luci                    luci-base                       luci-nginx              luci-ssl-nginx                          \
        luci-proto-qmi          luci-proto-3g                   luci-proto-ipv6         luci-proto-ppp  luci-proto-ncm          \
        luci-theme-bootstrap    luci-theme-openwrt-2020         luci-proto-bonding      \
\
        luci-app-udpxy          luci-app-advanced-reboot        luci-app-acme                   luci-app-adblock        luci-app-acl            \
        luci-app-aria2          luci-app-bcp38                  luci-app-attendedsysupgrade     luci-app-clamav         luci-app-commands       \
        luci-app-transmission           luci-app-dcwapd                 luci-app-ddns           luci-app-diag-core      \
        luci-app-dnscrypt-proxy luci-app-firewall               luci-app-frpc                   luci-app-frps           luci-app-fwknopd        \
        luci-app-hd-idle        luci-app-https-dns-proxy        luci-app-ksmbd                  luci-app-lxc            luci-app-minidlna       \
        luci-app-mwan3          luci-app-nextdns                luci-app-nft-qos                luci-app-nlbwmon        luci-app-nut            \
        luci-app-openvpn        luci-app-vnstat                 luci-app-opkg                   luci-app-qos            luci-app-squid          \
        luci-app-statistics     luci-app-upnp                   luci-app-mwan3\
\
        luci-mod-admin-full     luci-mod-network                luci-mod-status         luci-mod-system                 luci-mod-rpc            \
\
        luci-i18n-acl-ru                luci-i18n-acme-ru       luci-i18n-adblock-ru    luci-i18n-advanced-reboot-ru    luci-i18n-aria2-ru              \
        luci-i18n-attendedsysupgrade-ru luci-i18n-base-ru       luci-i18n-qos-ru        luci-i18n-bcp38-ru              luci-i18n-bmx7-ru               \
        luci-i18n-clamav-ru             luci-i18n-commands-ru   luci-i18n-dcwapd-ru             luci-i18n-ddns-ru               \
        luci-i18n-diag-core-ru          luci-i18n-firewall-ru   luci-i18n-frpc-ru       luci-i18n-frps-ru               luci-i18n-dnscrypt-proxy-ru     \
        luci-i18n-fwknopd-ru            luci-i18n-hd-idle-ru    luci-i18n-ksmbd-ru      luci-i18n-https-dns-proxy-ru    luci-i18n-lxc-ru                \
        luci-i18n-minidlna-ru           luci-i18n-mwan3-ru      luci-i18n-nextdns-ru    luci-i18n-nft-qos-ru            luci-i18n-nlbwmon-ru            \
        luci-i18n-nut-ru                luci-i18n-openvpn-ru    luci-i18n-opkg-ru       luci-i18n-pbr-ru                luci-i18n-mwan3-ru              \
'
#       -wpad-basic-mbedtls -libustream-mbedtls -libmbedtls     -cshark

FILES="files/"

make -j $(nproc) image PROFILE=generic ADD_LOCAL_KEY=1 BIN_DIR=$(pwd)/../images/ PACKAGES="$PACKAGES" FILES="$FILES"

Собираем образ, заливаем на USB Flash, загружаемся с него и далее заливаем образ на строенный ssd.

Далее, рекомендую создать дополнительный раздел, такого-же размера, как и rootfs. Это упростит дальнейшее обновление OpenWRT, т.к. стандартные механизмы для x86 не работают. Имея доп раздел, можно на него разворачивать новый rootfs, перекидывать конфиги и после этого переключать загрузку на использование второго rootfs. Первый, в это время, будет резервным старой версии. На этом, с OpenWRT всё. Все остальные его настройки у каждого свои :)

Установка Home Assistant

С этим у меня возникли некоторые сложности - предлагаемый вариант OVA образа работает в QEMU, но со сбоями - то перезагрузится, а то и вовсе - зависнет. Естественно, такой вариант для системы автоматизации ни как не годится. Посему, используем haos_generic-x86-64. Я остановился на пер релизе 10.0.rc3. Первым делом нам потребуется увеличить размер самого образа, что бы при первом запуске, автоматически, подстроился размер раздела (умолчательный в районе 6Гб, что маловато). Я свой образ растянул на 48Гб:

dd if=/dev/zero of=haos_generic-x86-64-10.0.rc3.img seek=HAsize bs=4k count=ResultSize

Здесь, HAsize - указываем размер образа в блоках (6442450944/4096=1572864), ResultSize = желаемый размер образа в блоках, в моём случае, для 48Гб: 12582912.

Далее, создаём скрип для запуска образа в qemu. Тут основная сложность в том, что большинство используют его через libvirt, а его нет собранного для OpenWRT, поэтому привожу сразу свой скрипт для запуска/завершения работы вирт.машины. Самое сложное в нём, это корректное выключение виртуальной машины. Располагаем скрипт в /etc/init.d/qemu-ha

#!/bin/sh /etc/rc.common

START=99
STOP=1

HA_DIR='/Path_where_your_HAOS_image_stored'
IMG='haos_generic-x86-64-10.0.rc3-48Gb.img'

QEMU_OPTS="-enable-kvm -cpu host -smp 2 -m 8G \
    -hda $HA_DIR/$IMG  \
    -device e1000,mac=E2:F2:6A:01:9D:C9,netdev=br0 \
    -netdev bridge,br=br-lan,id=br0 \
    -smbios type=0,uefi=on \
    -bios $HA_DIR/OVMF_CODE.fd \
    -action shutdown=poweroff \
    -qmp tcp:127.0.0.1:4444,server,nowait \
    -vnc :0 "

qemu_pidfile="/var/run/qemu.pid"

start() {
    /usr/bin/qemu-system-x86_64 $QEMU_OPTS \
        -daemonize &> /var/log/qemu.log

    /usr/bin/pgrep qemu-system-x86_64 > $qemu_pidfile
    echo "QEMU: Started VM with PID $(cat $qemu_pidfile)."
}

stop() {
    echo "QEMU: Sending 'system_powerdown' to VM with PID $(cat $qemu_pidfile)."

    nc 127.0.0.1 4444 <<QMP
{ "execute": "qmp_capabilities" }
{ "execute": "input-send-event",
     "arguments": { "events": [
        { "type": "key", "data" : { "down": true,
          "key": {"type": "qcode", "data": "power" } } } ] } }
QMP

    if [ "$?" == "0" ]; then
        if [ -e $qemu_pidfile ]; then
                if [ -e /proc/$(cat $qemu_pidfile) ]; then
                        echo "QEMU: Waiting for VM shutdown."
                        while [ -e /proc/$(cat $qemu_pidfile) ]; do sleep 1s; done
                        echo "QEMU: VM Process $(cat $qemu_pidfile) finished."
                else
                        echo "QEMU: Error: No VM with PID $(cat $qemu_pidfile) running."
                fi
            rm -f $qemu_pidfile
        else
                echo "QEMU: Error: $qemu_pidfile doesn't exist."
        fi
    else
        echo "QEMU: Error: QMP interface can't accept connections or unavailable."
    fi
}

Для автоматического запуска, выполняем: /etc/init.d/qemu-ha enable

Ну и запускаем машину: /etc/init.d/qemu-ha start

Для отладки, к машине можно подключиться по VNC, на пример, используя krdc.

И на последок - скрипт, для мониторинга температуры процессора в HA через mqtt:

#!/usr/bin/env python3
# coding=utf-8
# -*- coding: utf-8 -*-

import paho.mqtt.client as mqtt
import time
import json
import os, random

pid=os.getpid()
with open('/var/run/host_mon_mqtt.pid', "w") as f: f.write(str(pid))

broker_address="192.168.1.4"
topic = "Router state"
sensor_data = {'temperature_cpu': 0, 'temperature_board': 0}

########################################
#broker_address="iot.eclipse.org"
print("Creating new instance")
client = mqtt.Client(topic) #create new instance

print("connecting to broker")
client.connect(broker_address) #connect to broker

while not client.is_connected():
    client.loop()

client.loop_start() #start the loop

try:
    while True:
        with open('/sys/class/thermal/thermal_zone1/temp', "r") as f: temp1 = f.read().strip()
        with open('/sys/class/thermal/thermal_zone0/temp', "r") as f: temp2 = f.read().strip()
        if temp1: temp1 = round(int(temp1)/1000, 2)
        if temp2: temp2 = round(int(temp2)/1000, 2)
        if (temp1 != sensor_data['temperature_cpu'] or temp2 != sensor_data['temperature_board']):
                sensor_data['temperature_cpu'] = temp1
                sensor_data['temperature_board'] = temp2
                print("Temp is: ", temp1, temp2)
                client.publish('zigbee2mqtt/'+topic, json.dumps(sensor_data), 1)
        time.sleep(5)

except KeyboardInterrupt:
    pass

client.loop_stop()
client.disconnect()

И инит скрипт для него, располагаем /etc/init.d/host_mon_mqtt

#!/bin/sh /etc/rc.common

START=99
STOP=1

prog_name="mqtt-temp.py"
prog="/mnt/Storage/HomeAssistant/tools/${prog_name}"
pidfile="/var/run/host_mon_mqtt.pid"

start() {
    $prog > /var/log/${prog_name}.log 2>&1 &
    echo "Host monitoring to MQTT: Started with PID $(cat $pidfile)."
}

stop() {
    pid=$(cat $pidfile)
    echo "Host monitoring to MQTT: PID ${pid} shutdown."
    kill $pid
}

Заключение

На данный момент, система работает без сбоев. Запас производительности огромный. Всё стабильно. По завершению обкатки и после первых обновление OpenWRT, постараюсь дополнить.

По факту испытаний, выяснилось, что падения qemu были связаны с ошибками в microcode для kvm на процессорах Intel N5105. Версии микрокода до 0x24000024 (2022-09-02) падают. Лечится очень просто, скачиваем firmware вот тут. В каталог /lib/firmware/intel-ucode и будет стабильно работать.

Комментарии (10)


  1. Spider55
    13.04.2023 16:34

    Что-то не так с предлогами в заголовке...


    1. week123 Автор
      13.04.2023 16:34
      +1

      Спасибо. писал в угаре после нескольких дней настройки.


  1. Vovanys
    13.04.2023 16:34

    Про заметку про микрокод спасибо. У самого есть роутер на N6005 и раз в 1 или 2 недели виртуалка с MikroTik Chr ребуталась. Реально есть баг с этим микрокодом…


    1. week123 Автор
      13.04.2023 16:34

      Что бы микрокод автоматически подгружался, необходимо intel-microcode.bin переименовать в что-то типа 06-9c-00 (это для N5105). Требуемое имя можно получить:
      dmesg |grep microcode
      [    3.453122] microcode: sig=0x906c0, pf=0x1, revision=0x1d

      grep -E 'family|model|stepping' -m 3 /proc/cpuinfo
      cpu family      : 6
      model           : 156 -> 0x9c
      model name      : Intel(R) Celeron(R) N5105 @ 2.00GHz

      Intel microcode is named “[cpu family]-[model]-[stepping]”, using hexadecimal values. In the above output, this would be “06-3e-07”.

      Можно вручную загрузить:
      iucode_tool -K -S -l /lib/firmware/intel-ucode


  1. kharlashkin
    13.04.2023 16:34

    Подумал, запустили Home Assistant на роутере, а нет - показалось.


  1. suprimex
    13.04.2023 16:34

    Мммм непонял, для чего нужны танцы с QEMU ? есть же контеинеры под x386/64...


    1. week123 Автор
      13.04.2023 16:34

      Очень просто:

      1.Поднимать и поддерживать Docker под OpenWRT нету желания.

      2.Желание использовать систему обновления от HA.

      3.Переносимость и возможность отладки, просто скопировал образ, поднял на другом компе в виртуалке и тренируйся сколько влезет.


      1. edo1h
        13.04.2023 16:34

        Поднимать и поддерживать Docker под OpenWRT нету желания

        Посмотрел документацию, с первого взгляда всё просто

        https://openwrt.org/docs/guide-user/virtualization/docker_host


  1. DaemonGloom
    13.04.2023 16:34

    А из-за чего OpenWRT решили именно хостом сделать? Просто есть альтернативный вариант, избавляющий от проблем взаимодействия — хостом ставится любой гипервизор по вашему вкусу, а всё остальное — отдельными виртуальными машинами. Тогда виртуальный openwrt никак не мешает виртуальному же HA.


    1. week123 Автор
      13.04.2023 16:34

      1.Маршрутизатор не хотелось виртуализировать.

      2.Не всегда корректно работают, на пример, некоторые PCIe/USB модемы, если их пробрасывать в виртуальное окружение.

      3.В таком варианте потребуется обслуживание основной операционной системы, в том числе и построение firewall для неё.

      4.Потеря скорости, в моём случае не только трафик LAN-Internet. Ещё есть довольно много внутреннего LAN-LAN трафика, проходящего через маршрутизатор.