evgen@spb: ~/2026/07/kuklovodim-cherez-openvox-chast-4-lokalnoe-zerkalo/

Кукловодим через OpenVox. Часть 4. Локальное зеркало репозитория

Четвёртая, дополнительная часть цикла — про то, как перестать зависеть от внешнего репозитория. В предыдущих частях ноды качали агент с apt.voxpupuli.org, и это работало — но медленно, а иногда и с обрывами. Решение простое и знакомое любому, кто держал локальный apt-mirror: поднять своё зеркало и раздавать пакеты из собственной сети.

Схема получается такая: rsync тянет пакеты с Vox Pupuli, nginx раздаёт их по HTTP на нестандартном порту, а профиль Puppet прописывает ноды на локальный источник. Всё разворачивается на puppet-сервере (puppet.example.com) под root.

Синхронизация зеркала

Репозиторий Vox Pupuli устроен так: apt-часть лежит под apt/ (есть ещё yum/ и downloads/). Зеркалить весь репозиторий незачем — берём только apt/ и только компонент openvox8, а метаданные тянем целиком.

apt install -y rsync
mkdir -p /srv/openvox-mirror

rsync -avz --delete \
  --include='apt/' \
  --include='apt/GPG-KEY-openvox.pub' \
  --include='apt/openvox-keyring.gpg' \
  --include='apt/dists/***' \
  --include='apt/pool/' \
  --include='apt/pool/openvox8/***' \
  --exclude='*' \
  rsync://rsync.voxpupuli.org/packages/ /srv/openvox-mirror/

Проверяем, что зеркало приехало (apt-репозиторий физически лежит в /srv/openvox-mirror/apt/):

du -sh /srv/openvox-mirror                          # ~20 ГБ (все ОС + компоненты)
find /srv/openvox-mirror/apt/pool -name '*.deb' | head
ls /srv/openvox-mirror/apt/dists                    # debian11..13, ubuntu...

Про 20 гигабайт. В полную копию входят все поддерживаемые ОС и компоненты (server, db, bolt) плюс отладочные *-dbgsym*. Хотите сузить — добавьте --exclude='*-dbgsym*' и include только нужных ОС. Но обновления потом докачиваются инкрементально, так что полная копия особо не мешает — место есть, зато ничего не придётся доливать при подключении новой ОС.

Раздача через nginx

Ключевая тонкость — root должен указывать прямо на apt/. Тогда URL для нод получается чистым (/dists/..., а не /apt/dists/...).

apt install -y nginx

cat > /etc/nginx/sites-available/openvox-mirror <<'EOF'
server {
    listen 3333;
    listen [::]:3333;
    server_name puppet.example.com;

    root /srv/openvox-mirror/apt;
    autoindex on;

    location / {
        try_files $uri $uri/ =404;
    }
}
EOF

ln -sf /etc/nginx/sites-available/openvox-mirror /etc/nginx/sites-enabled/openvox-mirror
nginx -t
systemctl reload nginx

Проверяем, что зеркало отдаёт метаданные:

curl -sI http://puppet.example.com:3333/dists/debian13/InRelease    # HTTP/1.1 200 OK
curl -s  http://puppet.example.com:3333/dists/ | head

Порт 3333 нестандартный и по умолчанию закрыт — если есть файрвол, откройте его: ufw allow 3333/tcp или соответствующее правило nftables.

Регулярная синхронизация по cron

Зеркало должно обновляться само. Выносим rsync в скрипт и вешаем на cron:

cat > /usr/local/bin/openvox-mirror-sync.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
rsync -az --delete \
  --include='apt/' \
  --include='apt/GPG-KEY-openvox.pub' \
  --include='apt/openvox-keyring.gpg' \
  --include='apt/dists/***' \
  --include='apt/pool/' \
  --include='apt/pool/openvox8/***' \
  --exclude='*' \
  rsync://rsync.voxpupuli.org/packages/ /srv/openvox-mirror/
EOF
chmod +x /usr/local/bin/openvox-mirror-sync.sh

cat > /etc/cron.d/openvox-mirror <<'EOF'
17 4 * * * root /usr/local/bin/openvox-mirror-sync.sh >/var/log/openvox-mirror-sync.log 2>&1
EOF

Первичная синхронизация тянет все 20 ГБ, а дальше cron докачивает только новое — так что ежедневный прогон быстрый.

Переключение нод на зеркало

Ноды нужно перенаправить на локальный источник вместе с GPG-ключом — иначе apt справедливо откажется доверять неподписанному репозиторию. И здесь пряталась засада, на которую я потратил некоторое время:

Коварный нюанс с путём к ключу. Release-пакет OpenVox кладёт ключ в /etc/apt/keyrings/openvox-keyring.gpg, но в строке signed-by= некоторых версий прописан /usr/share/keyrings/... — путь, которого на ноде нет. Симптом: Failed to parse keyring ... No such file or directory и жалоба, что «репозиторий не подписан». Профиль ниже кладёт и ключ, и источник в согласованные пути, разом устраняя эту проблему.

Кладём ключ в control-repo

Ключ будем раздавать нодам через Puppet, поэтому кладём его в файлы модуля:

cd /root/control-repo
mkdir -p site-modules/profile/files
cp /etc/apt/keyrings/openvox-keyring.gpg site-modules/profile/files/openvox-keyring.gpg

Профиль profile::openvox_mirror

site-modules/profile/manifests/openvox_mirror.pp:

class profile::openvox_mirror (
  String $mirror_url = 'http://puppet.example.com:3333',
) {
  # suite = имя каталога dists/ под конкретную ОС
  $suite = $facts['os']['name'] ? {
    'Debian' => "debian${facts['os']['release']['major']}",
    'Ubuntu' => "ubuntu${facts['os']['release']['full']}",   # напр. ubuntu24.04
    default  => undef,
  }

  if $suite == undef {
    warning("profile::openvox_mirror: неподдерживаемая ОС ${facts['os']['name']}")
  } else {
    file { '/etc/apt/keyrings':
      ensure => directory,
      owner  => 'root',
      group  => 'root',
      mode   => '0755',
    }

    file { '/etc/apt/keyrings/openvox-keyring.gpg':
      ensure  => file,
      owner   => 'root',
      group   => 'root',
      mode    => '0644',
      source  => 'puppet:///modules/profile/openvox-keyring.gpg',
      require => File['/etc/apt/keyrings'],
    }

    file { '/etc/apt/sources.list.d/openvox8-release.list':
      ensure  => file,
      owner   => 'root',
      group   => 'root',
      mode    => '0644',
      content => "# Managed by OpenVox\ndeb [signed-by=/etc/apt/keyrings/openvox-keyring.gpg] ${mirror_url}/ ${suite} openvox8\n",
      require => File['/etc/apt/keyrings/openvox-keyring.gpg'],
    }
  }
}

Обратите внимание на $suite: для Debian имя каталога собирается из мажорной версии (debian13), а для Ubuntu — из полной (ubuntu24.04). Это соответствует тому, как каталоги названы в dists/ на зеркале, и разведено селектором по факту ОС.

Подключаем профиль в profile::base:

class profile::base {
  include profile::openvox_mirror
  include profile::users
  include profile::packages
  include profile::rsyslog
  include profile::zabbix_agent
}

И раскатываем привычным циклом:

git add -A && git commit -m 'Add profile::openvox_mirror'
git push origin production
/opt/puppetlabs/puppet/bin/r10k deploy environment production -pv

Проверка

На любой ноде после прогона:

puppet agent -t
apt update                        # без ошибок про подпись
apt policy openvox-agent          # источник должен быть puppet.example.com:3333

Ожидаемый вывод apt policy — версии агента отдаются с локального зеркала:

*** 8.28.1-1+debian13 1001
    500 http://puppet.example.com:3333 debian13/openvox8 amd64 Packages

Быстрое переключение одной ноды вручную

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

# заменить хост
sed -i 's|https://apt.voxpupuli.org/|http://puppet.example.com:3333/|' \
  /etc/apt/sources.list.d/openvox*.list

# проверить путь к ключу в signed-by и привести к реальному расположению
grep signed-by /etc/apt/sources.list.d/openvox*.list
ls -la /etc/apt/keyrings/openvox-keyring.gpg      # где ключ реально лежит

# если пути разошлись — поправить signed-by на реальный путь
sed -i 's|/usr/share/keyrings/openvox-keyring.gpg|/etc/apt/keyrings/openvox-keyring.gpg|' \
  /etc/apt/sources.list.d/openvox*.list

apt update
apt policy openvox-agent

Что в итоге

Приятная деталь: профиль не конфликтует с чужими репозиториями — он трогает только источник OpenVox, а репозитории Zabbix, Debian и прочие на нодах не затрагивает.

На этом цикл про OpenVox можно считать законченным. Мы прошли путь от установки сервера до полностью автономной инфраструктуры: свой сервер управления, ноды под контролем, настройки в виде кода и даже пакеты, раздаваемые из собственной сети. Внешний мир теперь нужен этой конструкции по минимуму — ровно так, как и должно быть у инфраструктуры, за которую отвечаешь сам.

evgen@spb: ~/contacts
evgen@spbping -c1 evgen.info
64 bytes from evgen.info: отвечу в течение рабочего дня
evgen@spbcat contacts.txt
mail      ya@evgen.info
telegram  @EvgenOne
github    github.com/onegin
blog      evgen.info/blog
evgen@spbecho $ЗАЧЕМ_ПИСАТЬ
интересная инфраструктурная задача · вопрос по Proxmox или Icinga ·
желание обсудить, почему memtest молчал, а память была битая
evgen@spb
© 2026 Евгений Подолинский · Санкт-Петербург
синий здесь – не тот, о котором вы подумали.