Продвинутый

Claw-Code: автономный музейный стенд на Rust

Алексей Кузнецов
Алексей Кузнецов
Системный администратор30 октября 2026 г.10 мин чтения

Автономный музейный стенд, разработанный в Rust с Gajae-Code/LazyCodex, работает без участия человека, обеспечивая предсказуемую инфраструктуру.

Claw-Code: автономный музейный стенд на Rust

Автономный музейный стенд, разработанный в Rust с Gajae-Code/LazyCodex, работает без участия человека, обеспечивая предсказуемую инфраструктуру.

Архитектура и технологии

Архитектура и технологии

### Rust‑основа и Gajae-Code

Для автономного стенда выбран Rust – компилируемый язык, обеспечивающий предсказуемую работу без ручного вмешательства. В качестве фреймворка используется Gajae-Code, который предоставляет готовые компоненты для управления состоянием службы, логирования и динамической конфигурации.

Cargo.toml (пример зависимости):

toml
[package]
name = "claw-code"
version = "0.1.0"
edition = "2021"

[dependencies]
gajae-code = "0.3"
tokio = { version = "1.38", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

systemd‑юнит для автозапуска сервиса на домашнем сервере (например, на Raspberry Pi):

ini
[Unit]
Description=Claw‑Code autonomous museum stand
After=network.target

[Service]
ExecStart=/usr/local/bin/claw-code --config /etc/claw-code/config.toml
Restart=on-failure
User=clawuser
Group=clawuser
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target

Пример конфигурации в TOML‑файле (/etc/claw-code/config.toml):

toml
[display]
title = "Русский музейный стенд"
locale = "ru_RU"

[agents]
enabled = true
poll_interval = "30s"

Важно: все пути к файлам и переменные окружения следует указывать в коде явно, иначе сервис может не стартовать при загрузке.

### LazyCodex и агент‑управление

LazyCodex – утилита, генерирующая скрипты‑агенты на основе шаблонов YAML. Она позволяет описать набор действий, которые должны выполнять автономные процессы (например, сбор данных, обновление экспозиций). Сгенерированные агенты запускаются через лёгкий Rust‑сервис, который обеспечивает их координацию и мониторинг.

Пример шаблона агента (agents/lazy-agent.yaml):

yaml
agent_id: "photo-uploader"
type: "file_transfer"
source: "/data/captured"
destination: "s3://museum-backup/photos"
schedule: "0 */6 * * *"

Команда запуска менеджера агентов (bash):

bash
cargo run --bin agent-manager -- --config /etc/claw-code/agents.yaml

Логика работы: при получении сигнала SIGUSR1 менеджер перечитывает конфигурацию, что позволяет динамически добавлять или удалять агенты без рестарта основного сервиса. Это обеспечивает предсказуемость и упрощает обслуживание в домашней лаборатории.

Ссылка на документацию: Gajae-Code docs
Ссылка на LazyCodex: LazyCodex repo

Таким образом, сочетание Rust‑основы, Gajae-Code и LazyCodex образует надёжную, самодостаточную инфраструктуру, готовую к работе в автономном режиме без постоянного участия человека.

Разработка без вмешательства человека

Автономный планировщик задач

Во-первых, автономный планировщик – это небольшая Rust‑программа, которая в цикле читает описание задач, формирует их порядок и запускает нужные команды.
Для простоты я использую файл tasks.toml в формате TOML, который удобно редактировать вручную.

toml
# tasks.toml
[task "backup_photos"]
command = "python3 /opt/backup/photos.py"
schedule = "0 2 * * *"   # каждый день в 02:00

[task "update_services"]
command = "docker compose -f /opt/claw-code/docker-compose.yml pull && docker compose -f /opt/claw-code/docker-compose.yml up -d"
schedule = "*/15 * * * *" # каждые 15 минут

Программа парсит файл, формирует вектор задач и в отдельном потоке каждые 30 секунд проверяет, пора ли выполнять задачу. Ниже минимальный пример на Rust с crate serde и tokio.

rust
use serde::Deserialize;
use std::time::Duration;
use tokio::time;

#[derive(Deserialize, Debug)]
struct Task {
    command: String,
    schedule: String,
}

#[derive(Deserialize, Debug)]
struct Config {
    tasks: Vec<Task>,
}

async fn run_task(task: &Task) {
    println!("Запуск: {}", task.command);
    // простой способ – выполнить через системный процесс
    std::process::Command::new("sh")
        .arg("-c")
        .arg(&task.command)
        .spawn()
        .expect("failed to spawn command");
}

#[tokio::main]
async fn main() {
    let cfg: Config = toml::from_str(&std::fs::read_to_string("tasks.toml").unwrap()).unwrap();

    loop {
        let now = chrono::Local::now();
        for task in &cfg.tasks {
            // простейшая проверка расписания – раз в минуту
            if now.minute() % 30 == 0 && now.second() < 5 {
                // здесь можно добавить более точную проверку crontab‑подобного расписания
                // для примера просто выводим сообщение
                println!("Проверка задачи: {}", task.command);
                // в реальном коде вызываем run_task(&task).await;
            }
        }
        time::sleep(Duration::from_secs(30)).await;
    }
}

Для того, чтобы планировщик не блокировал основные сервисы, я запускаю его как systemd‑службу. Пример unit‑файла:

ini
# /etc/systemd/system/claw-scheduler.service
[Unit]
Description=Автономный планировщик задач Claw‑Code
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/claw-scheduler
Restart=on-failure
User=alexey
WorkingDirectory=/opt/claw-code

[Install]
WantedBy=multi-user.target

И таймер, который будет запускать сервис каждый час (можно реже, если задачи реже):

ini
# /etc/systemd/system/claw-scheduler.timer
[Unit]
Description=Таймер для авто­много планировщика

[Timer]
OnCalendar=hourly
Persistent=true

[Install]
WantedBy=timers.target

После systemctl daemon-reload и systemctl enable --now claw-scheduler.service claw-scheduler.timer планировщик будет работать автономно, а systemd гарантирует его перезапуск при падении.


CI/CD в домашней лаборатории

Справедливость и предсказуемость инфраструктуры достигаются тогда, когда каждый коммит проходит через автоматизированный пайплайн. В домашней лаборатории я использую размещение на домашних серверах runner GitHub Actions, развернутый на отдельном VM Proxmox, и Docker‑композ для изоляции всех сервисов.

1. размещение на домашних серверах runner

На VMProxmox создаю образ Ubuntu 22.04, устанавливаю actions-runner и регистрирую его в репозитории. Инструкция официальная, но основные шаги:

bash
# скачивание runner
mkdir actions && cd actions
curl -L -o actions-runner-linux-x64-2.306.0.tar.gz \
  https://github.com/actions/runner/releases/download/v2.306.0/actions-runner-linux-x64-2.306.0.tar.gz
tar -zxvf actions-runner-linux-x64-2.306.0.tar.gz
./config.sh --url https://github.com/your‑org --token YOUR_TOKEN --name claw‑runner --labels linux,home‑lab
./run.sh

После запуска runner будет прослушивать очередь GitHub Actions и выполнять задачи.

2. Docker‑Compose для CI

Для изоляции билд‑окружения использую docker-compose.yml, который поднимает локальный регистр и контейнер‑билдер.

yaml
# docker-compose.yml
version: "3.9"
services:
  registry:
    image: registry:2
    restart: always
    ports:
      - "5000:5000"
    volumes:
      - registry-data:/var/lib/registry

  builder:
    image: mcr.microsoft.com/dotnet/sdk:8.0   # пример для .NET, заменяется под ваш стек
    working_dir: /src
    volumes:
      - .:/src
    command: ["bash", "-c", "dotnet build -c Release && docker build -t localhost:5000/claw-service ."]
    depends_on:
      - registry

volumes:
  registry-data:

В workflow я указываю, что нужно использовать сервисы из этого compose‑файла, а затем пушить готовый образ в локальный регистр.

3. Пример GitHub Actions workflow

yaml
# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: claw-runner   # размещение на домашних серверах runner
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to local registry
        run: echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin localhost:5000

      - name: Build and push image
        run: |
          docker compose -f docker-compose.yml run --rm builder
          docker push localhost:5000/claw-service:${{ github.sha }}

4. Как это выглядит в реальной работе

После коммита в main runner автоматически поднимает контейнер‑билдер, собирает образ claw-service, тегирует его SHA‑хешем и пушит в локальный регистр localhost:5000. Далее любой сервис, запущенный в Proxmox, может просто выполнить docker pull localhost:5000/claw-service:<sha> и обновиться без вмешательства человека.

Таким образом, CI/CD в домашней лаборатории обеспечивает:

  • Предсказуемость – каждый билд проходит одинаковый набор шагов.
  • Автономность – runner работает в изолированном контейнере, а не на локальном рабочем столе.
  • Трассировку – логи Docker‑компонов и GitHub Actions легко собрать в Grafana + Loki.

Соблюдая эти подходы, мы получаем полностью автономную инфраструктуру, где планировщик задач и CI/CD работают без постоянного контроля, а серверы остаются предсказуемыми и стабильными.

Интеграция с инфраструктурой

ZFS‑хранилище и NFS‑доступ

Во-первых, ZFS обеспечивает предсказуемую целостность данных за счёт копирования блоков и встроенных проверок контрольных сумм. Для небольшого музея достаточно создать отдельный пул и датасет, где включить сжатие lz4 — это экономит место без потери скорости.

bash
# Создание пула из двух дисков (пример по‑идентификатору)
zpool create -f -o ashift=12 tank mirror \
  /dev/disk/by-id/ata-WDC_WD50EFRX-68JX4N0-EX4W6030-00140 \
  /dev/disk/by-id/ata-WDC_WD50EFRX-68JX4N0-EX4W6030-00141

# Датасет для музейных файлов с сжатием и оптимизацией под большие файлы
zfs create -o compression=lz4 -o recordsize=128K -o atime=off tank/museum

После создания пула его можно экспортировать через NFS, чтобы клиентские станции (например, Raspberry Pi‑кейсы) монтировали его как обычную папку.

bash
# Добавление экспорта в /etc/exports (192.168.1.0/24 — подсеть музея)
echo "tank/museum 192.168.1.0/24(rw,sync,no_subtree_check)" >> /etc/exports
exportfs -ra

# На клиенте монтируем
mount -t nfs server:/tank/museum /mnt/museum

Справедливая точка зрения: NFS‑доступ прост в настройке, но требует постоянного контроля за состоянием экспорта. Поэтому удобно автоматизировать проверку через systemd‑unit, которая будет перезапускать экспорт при потере сети.

ini
# /etc/systemd/system/nfs-export.service
[Unit]
Description=NFS export for museum dataset
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/exportfs -ra
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Мониторинг и алерты через Prometheus

С практической точки зрения мониторинг состояния ZFS и NFS позволяет быстро реагировать на деградацию пула или потерю экспорта. Prometheus + node_exporter — базовый стек, а для специфических метрик удобно использовать textfile‑collector.

Метрики ZFS

ZFS‑метаданные экспортируются в виде текстового файла, который node_exporter читает через --collector.textfile.directory. Пример файла /var/lib/node_exporter/textfile/zfs.prom:

# ZFS pool health (0 — HEALTHY, 1 — DEGRADED, 2 — FAULTED)
zfs_pool_state{pool="tank"} 0

Alert‑правила

yaml
# alerts.yml
groups:
- name: museum_infrastructure
  rules:
  - alert: ZFSPoolDegraded
    expr: zfs_pool_state{pool="tank"} == 1
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "ZFS pool {{ $labels.pool }} is degraded"
      description: "Pool {{ $labels.pool }} reported degraded state for more than 5 minutes."

Эти правила попадают в конфигурацию Prometheus:

yaml
# prometheus.yml (фрагмент)
scrape_configs:
  - job_name: 'node'
    static_configs:
      - targets: ['192.168.1.10:9100']
    scheme: http
    relabel_configs:
      - source_labels: [__address__]
        target_label: __metrics_path__
        replacement: /metrics

Если алерт сработал, Prometheus отправит уведомление в Alertmanager, а тот в свою очередь может вызвать webhook‑скрипт, который перезапустит NFS‑export или инициирует автоматическое переключение на резервный пул.

Практический пример

Допустим, в логе node_exporter появилась строка zfs_pool_state{pool="tank"} 1. Через 5 минут Prometheus сработает на правило ZFSPoolDegraded, а Alertmanager отправит сообщение в Slack‑канал #museum-ops. Оператор увидит, что один из дисков в пуле tank вышел из строя, и сможет заменить его без простоя сервиса.

Таким образом, сочетание ZFS + NFS и мониторинга в Prometheus обеспечивает полностью автономную инфраструктуру, где любые отклонения от предсказуемого состояния сразу попадают в поле зрения оператора.

Практические примеры и кейсы

Практические примеры и кейсы

### Пример: интерактивный экспонат с датчиками

Для создания интерактивного экспоната используем Raspberry Pi 4, датчик DHT22 (температура / влажность) и Rust‑приложение, которое читает данные через GPIO и отдаёт их по простому HTTP‑серверу.

Шаг 1. Подготовка оборудования
Подключаем датчик к пину GPIO 4 (для Pi 4) согласно схеме:

  • VCC → 3.3 V
  • GND → GND
  • DATA → GPIO 4 (с pull‑up ≈ 10 kΩ).

Шаг 2. Cargo.toml

toml
[package]
name = "museum-exhibit"
version = "0.1.0"
edition = "2021"

[dependencies]
rppal = "0.15"
tiny_http = "0.12"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Шаг 3. Rust‑код

rust
use rppal::gpio::{Gpio, Pin, Mode};
use tiny_http::{Response, Server};
use serde::Serialize;
use std::thread;
use std::time::Duration;

#[derive(Serialize)]
struct SensorData {
    temperature: f32,
    humidity: f32,
}

fn read_sensor(gpio: &Gpio) -> SensorData {
    let pin = gpio.get(4).unwrap(); // номер pin'а
    let mut r = pin.read().unwrap(); // 0‑1 уровень
    // простая обработка: ждём несколько раз, чтобы стабилизировать сигнал
    thread::sleep(Duration::from_millis(200));
    let value = r.value();
    // преобразуем цифровое значение в физические параметры (пример)
    let temperature = -273.15 + (value as f32) * 0.1; // условно
    let humidity = (value as f32) * 0.01; // условно
    SensorData {
        temperature,
        humidity,
    }
}

fn main() {
    let gpio = Gpio::new().unwrap();
    let server = Server::http("0.0.0.0:8080").unwrap();

    println!("Exhibit server listening on port 8080");
    for request in server.incoming_requests() {
        let data = read_sensor(&gpio);
        let response = Response::from_string(serde_json::to_string(&data).unwrap());
        request.respond(response).unwrap();
    }
}

Шаг 4. systemd‑юнит для автозапуска

ini
[Unit]
Description=Museum interactive exhibit
After=network.target

[Service]
ExecStart=/usr/bin/cargo run --release --manifest-path /home/alexey/museum-exhibit/Cargo.toml
WorkingDirectory=/home/alexey/museum-exhibit
Restart=always
User=alexey
Group=alexey

[Install]
WantedBy=multi-user.target

После размещения юнита (/etc/systemd/system/museum-exhibit.service) выполняем:

bash
sudo systemctl daemon-reload
sudo systemctl enable museum-exhibit.service
sudo systemctl start museum-exhibit.service

Экспонат теперь доступен по адресу http://<IP_Pi>:8080 и отдаёт текущие значения температуры и влажности в формате JSON.

### Ограничения и возможные улучшения

ОграничениеВозможное улучшение
Питание – при отключении электроэнергии данные теряются.Установить небольшой UPS (например, на базе Li‑ion) или использовать аккумулятор с автозапуском.
Точность датчика – DHT22 имеет погрешность ± 0.5 °C и ± 2 % RH.Перейти на более точный цифровой датчик (например, Sensirion SHT31) и интегрировать через I²C.
Отсутствие масштабируемости – один Pi 4 ограничивает количество датчиков.Разместить датчики на нескольких узлах (Raspberry Pi Zero) и оркестровать их через k3s, а результаты собирать в центральный Prometheus‑сервер.
Сетевая уязвимость – открытый HTTP‑порт без аутентификации.Добавить TLS (использовать rustls или обратный прокси Caddy с автоподписанным сертификатом) и basic‑auth.
Логирование – только вывод в консоль.Перенаправить логи в journald и отправлять в Loki, добавить метрики (температура, время отклика) в Prometheus.
Обновление прошивки – требуется физический доступ.Реализовать OTA‑обновление через systemd‑unit, использующую rsync или mender.
Надёжность – отсутствие watchdog‑механизма.Включить системный watchdog (systemd-watchdog) и/или аппаратный watchdog на уровне Pi (через dtparam=watchdog).

Эти меры повышают надёжность и предсказуемость автономного стенда, позволяя ему работать без вмешательства человека в течение длительного периода.

Поделиться:TelegramX / TwitterVK