Работа с RPM (dnf / yum)

CodeScoring.Save работает как обычный rpm-md-репозиторий — тот, у которого в корне лежит repodata/repomd.xml. Его понимают dnf и yum на RHEL, Rocky Linux, AlmaLinux, Fedora, CentOS и других дистрибутивах семейства. Можно создать:

  • proxy-репозиторий — кэширующее зеркало репозитория дистрибутива или стороннего репозитория (EPEL, репозитории вендоров, Nexus, Artifactory, другой Save);
  • hosted-репозиторий — хранилище для собственных пакетов компании. createrepo запускать не нужно: метаданные Save собирает сам после каждой загрузки.

Создание репозитория в веб-интерфейсе

  1. В боковом меню выберите Projects и откройте проект.
  2. Нажмите Create repository.
  3. Заполните общие поля (подробнее — в разделе Управление репозиториями):
    • Name — название репозитория, например Rocky 9 BaseOS. Если в названии есть формат и тип (например, my RPM proxy), ссылка Click here to automatically fill the format (rpm) and type (proxy) based on the name под полем сама заполнит Format, Type и Proxy URL;
    • Key — идентификатор в URL, например rocky9-baseos. Если оставить пустым, сгенерируется из названия. Изменить после создания нельзя;
    • Description и Color — по желанию.
  4. В поле Format выберите RPM.
  5. В поле Type выберите Proxy или Hosted.

Поля proxy-репозитория

  • Proxy URL — baseurl репозитория, который нужно проксировать: каталог, внутри которого лежит repodata/repomd.xml. Ссылка автозаполнения под Name подставляет сюда пример — BaseOS для Rocky Linux 9; иначе поле пустое.

    Готовый адрес удобно взять с машины, где этот репозиторий уже подключён: dnf repolist -v показывает его в строке Repo-baseurl. Учтите:

    • в Proxy URL переменные $releasever и $basearch не подставляются. Для каждого сочетания версии и архитектуры (и для каждого репозитория: BaseOS, AppStream, EPEL…) создайте отдельный proxy-репозиторий;
    • нужен конкретный baseurl: адреса mirrorlist и metalink не подойдут;
    • репозитории, которые требуют клиентский TLS-сертификат (например, CDN Red Hat по подписке), проксировать нельзя.
  • Cache TTL, seconds — как часто Save перепроверяет метаданные (repomd.xml) у upstream. Пакеты не меняются и кэшируются навсегда. Для репозиториев дистрибутивов подойдёт 3600 (час).

  • Authentication — как Save авторизуется в upstream. Для публичных зеркал оставьте none.

    ЗначениеПоляЧто отправляется в upstreamКогда выбирать
    none—Запросы без авторизацииПубличные зеркала дистрибутивов, EPEL
    basicUsername, PasswordAuthorization: Basic ...Upstream принимает логин и пароль — так же, как опции username и password в .repo-файле. Это случай Nexus, Artifactory и другого Save
    bearerTokenAuthorization: Bearer <token>Upstream принимает токен в заголовке Bearer

    Пароль и токен после сохранения не показываются ни в интерфейсе, ни в API: на форме редактирования сохранённое значение отображается как ******** — оставьте его, чтобы не менять. Учётные данные Save отправляет только на хост из Proxy URL (подробнее — Доступ proxy-репозиториев к upstream).

Поля hosted-репозитория

  • RPM metadata folder depth — глубина корня индексации. Оставьте пустым (или 0), если нужен один repodata/ на весь репозиторий — так подходит в большинстве случаев. Если в одном репозитории будут пакеты для нескольких дистрибутивов, см. Несколько корней индексации.
  1. При необходимости привяжите политики очистки в блоке Cleanup policy: перенесите нужные из списка Available в Applied.
  2. Нажмите Create repository.

На странице созданного репозитория кнопка со значком «звено цепи» показывает готовый .repo-файл и команды именно для этого репозитория.

Создание через API

Те же репозитории можно создать запросом к API. В format указывается rpm, учётные данные upstream передаются в блоке auth, глубина корня — в settings.rpm_root_depth:

# Proxy
curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "infra",
    "name": "rocky9-baseos",
    "format": "rpm",
    "repository_type": "proxy",
    "remote_url": "https://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os",
    "cache_ttl": 3600
  }'

# Hosted
curl -X POST https://save.example.com/api/v1/repos \
  -H "Content-Type: application/json" \
  -u "<username>:<password>" \
  -d '{
    "project": "infra",
    "name": "rpm-hosted",
    "format": "rpm",
    "repository_type": "hosted"
  }'

Для proxy с авторизацией добавьте "auth": {"auth_type": "basic", "username": "<upstream-user>", "password": "<upstream-password>"}. Для hosted с несколькими корнями — "settings": {"rpm_root_depth": 2}.

Подключение dnf / yum

Адрес репозитория в Save имеет вид https://save.example.com/cs-save/rpm/<project>/<repository> — это и есть baseurl для клиента. Создайте файл /etc/yum.repos.d/codescoring.repo:

[codescoring-baseos]
name=Rocky 9 BaseOS (CodeScoring Save)
baseurl=https://save.example.com/cs-save/rpm/<project>/rocky9-baseos
enabled=1
username=<username>
password=<password>
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-Rocky-9
dnf makecache --repo=codescoring-baseos
dnf install <package>

Чтобы все пакеты шли через Save, отключите исходные репозитории дистрибутива (enabled=0 в их .repo-файлах) или замените в них baseurl / mirrorlist на адрес Save. Описание всех опций .repo-файла — в документации dnf.

Robot-аккаунты в CI

Для CI/CD можно использовать robot-аккаунт: username = sa$<robot-login>, password = <api-key> в .repo-файле. Структура файла не меняется — отличаются только значения. Подробнее — в общем разделе Аутентификация.

Проверка подписей

  • Proxy. Save отдаёт пакеты и метаданные upstream без изменений, включая repomd.xml.asc. Поэтому ключи дистрибутива продолжают работать как раньше: gpgcheck=1 с gpgkey дистрибутива, а если upstream подписывает метаданные — и repo_gpgcheck=1.

  • Hosted. Save не подписывает и не изменяет пакеты. Чтобы использовать gpgcheck=1, подпишите пакеты до загрузки (rpmsign --addsign) и раздайте свой публичный ключ клиентам.

  • Подпись метаданных hosted-репозитория. Если на сервере включена подпись метаданных (METADATA_SIGNING_ENABLED), Save подписывает repomd.xml своим ключом (repodata/repomd.xml.asc). Публичный ключ доступен по адресу <baseurl>/repository.key. Включите проверку так:

    repo_gpgcheck=1
    gpgkey=https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repository.key

    repo_gpgcheck проверяет подпись метаданных, gpgcheck — подпись самих пакетов. Это разные ключи: если пакеты подписаны вашим ключом, перечислите в gpgkey оба через пробел.

Публикация пакетов в hosted-репозиторий

У dnf нет команды публикации: как и в Nexus или Artifactory, пакет загружается обычным HTTP-запросом PUT на путь, где он будет лежать в репозитории. Параметр -T (--upload-file) в curl отправляет файл именно так:

curl -u "<username>:<password>" \
  -T mypackage-1.0.0-1.el9.x86_64.rpm \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/Packages/m/mypackage-1.0.0-1.el9.x86_64.rpm

После загрузки Save в течение нескольких секунд пересобирает repodata/ — createrepo_c запускать не нужно. На клиенте выполните dnf makecache --refresh, чтобы получить новые метаданные сразу.

Что проверяет Save при загрузке:

  • имя файла — в формате NVRA <name>-<version>-<release>.<arch>.rpm, и оно должно совпадать с заголовком пакета. Правильное имя для файла можно получить так: rpm -qp --queryformat '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}.rpm\n' <file>.rpm;
  • путь Packages/... (как в примере выше) или просто имя файла в корне Save приводит к стандартному виду Packages/<первая буква>/<файл>. Любой другой каталог сохраняется как есть — это нужно для нескольких корней индексации;
  • повторная загрузка того же пакета вернёт 409.

Удалить пакет можно в веб-интерфейсе (Delete artifact на странице репозитория) — метаданные пересоберутся автоматически.

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

curl -u "<username>:<password>" \
  -X POST https://save.example.com/cs-save/rpm/<project>/rpm-hosted/rebuild-index

Пересборка также удаляет старые файлы repodata/, на которые больше не ссылается repomd.xml.

Несколько корней индексации

По умолчанию у hosted-репозитория один repodata/ в корне. Если в одном репозитории хранятся пакеты для разных дистрибутивов или версий, удобнее, чтобы у каждого был свой repodata/. Для этого задайте RPM metadata folder depth — аналог yumRootDepth в Artifactory и Repodata Depth в Nexus.

Глубина — это число первых каталогов в пути пакета, которые образуют корень. Например, при глубине 2:

Путь пакетаКореньbaseurl для клиента
centos/7/Packages/hello-1.0-1.el7.x86_64.rpmcentos/7https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/7
centos/8/Packages/hello-1.0-1.el8.x86_64.rpmcentos/8https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/8

У каждого корня свой независимый repodata/: пакеты из centos/7 не попадают в индекс centos/8.

  • 0 (по умолчанию) — один repodata/ на весь репозиторий.
  • 1–10 — отдельный repodata/ для каждого корня этой глубины.
  • Настройка есть только у hosted-репозиториев: proxy отдаёт метаданные upstream как есть.

Пакет загружается в каталог своего корня:

curl -u "<username>:<password>" \
  -T hello-1.0-1.el7.x86_64.rpm \
  https://save.example.com/cs-save/rpm/<project>/rpm-dists/centos/7/Packages/hello-1.0-1.el7.x86_64.rpm

Если глубина больше 0, путь пакета должен содержать не меньше каталогов, чем задано глубиной, и не должен начинаться с Packages/. Иначе Save вернёт 400 с подсказкой, куда загрузить пакет. Artifactory в таком случае сохраняет пакет без индексации, а Save отклоняет загрузку специально: иначе пакет «загружен», но dnf его не видит. Один и тот же пакет можно загрузить в разные корни.

Глубину можно изменить и у существующего репозитория на форме Edit repository. Save переиндексирует репозиторий в фоне: создаст repodata/ для новых корней и удалит метаданные старой схемы. Пока переиндексация не завершилась, новые repomd.xml могут быть ещё недоступны.

Если включена подпись метаданных, repomd.xml.asc публикуется в каждом корне, а ключ один на весь репозиторий: он доступен и по адресу <repo>/repository.key, и по адресу <корень>/repository.key. rebuild-index пересобирает метаданные всех корней.

Миграция URL репозитория

Сценарий использования: миграция RPM-репозитория с Nexus / Artifactory на CodeScoring.Save.

Источникbaseurl до миграцииbaseurl после миграции
Nexushttps://nexus.host.ru/repository/yum-hostedhttps://save.example.com/cs-save/rpm/<project>/rpm-hosted
Artifactoryhttps://jfrog.host.ru/artifactory/rpm-localhttps://save.example.com/cs-save/rpm/<project>/rpm-hosted
Официальное зеркалоhttps://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/oshttps://save.example.com/cs-save/rpm/<project>/rocky9-baseos

Параметры аутентификации (username / password) в .repo-файле сохраняются без изменений. Если в Nexus или Artifactory использовалась глубина repodata (Repodata Depth / yumRootDepth), задайте такое же значение в RPM metadata folder depth и загрузите пакеты по тем же путям — baseurl корней сохранятся.

Устранение неполадок

Что делает dnf

dnf clean metadata --repo=codescoring-baseos
dnf -v makecache --repo=codescoring-baseos

С ключом -v dnf показывает адреса, к которым обращается, и причину ошибки. 401 означает, что не подошли username / password в .repo-файле.

Проверка repomd.xml

curl -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repodata/repomd.xml

В ответе — XML с разделами primary, filelists, other. 404 у hosted-репозитория с глубиной больше 0 — ожидаемо: метаданные лежат в каталогах корней (<корень>/repodata/repomd.xml).

Пакет загружен, но dnf его не видит

Подождите несколько секунд (метаданные собираются асинхронно), выполните dnf makecache --refresh или rebuild-index. Проверить, есть ли пакет в индексе:

# href primary-индекса берётся из repomd.xml
curl -s -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repodata/<hash>-primary.xml.gz \
  | gunzip | grep '<location'

Проверка публичного ключа

curl -u "<username>:<password>" \
  https://save.example.com/cs-save/rpm/<project>/rpm-hosted/repository.key
# Ожидается: -----BEGIN PGP PUBLIC KEY BLOCK-----
# 404 означает, что подпись метаданных не включена на сервере

Состояние сервиса

curl https://save.example.com/health

Аудит по репозиторию

curl -u "<username>:<password>" \
  "https://save.example.com/api/v1/admin/audit?resource_type=repository&q=rpm-hosted&limit=50"
Страница была полезна?