7 Коммитов
Автор SHA1 Сообщение Дата
viktor d50a4314aa Сборка ISO не принимает системные имена администратора (admin, root, kvm): установщик Ubuntu их отвергает, и автоматическая установка падала на первом шаге
CI / Tests (Python 3.11) (push) Canceled after 0s
CI / Tests (Python 3.12) (push) Canceled after 0s
CI / Tests (Python 3.13) (push) Canceled after 0s
CI / Tests (Python 3.14) (push) Canceled after 0s
CI / Shell scripts (push) Canceled after 0s
Build ISO image / ISO ${{ fromJSON(github.event_name == 'workflow_dispatch' && (inputs.arch == 'both' && '["amd64","arm64"]' || format('["{0}"]', inputs.arch)) || '["amd64","arm64"]') }} (push) Canceled after 0s
2026-09-27 15:54:01 +09:00
viktor 40978d1c2b Версия была обновлена до 0.11.1
CI / Tests (Python 3.11) (push) Canceled after 0s
CI / Tests (Python 3.12) (push) Canceled after 0s
CI / Tests (Python 3.13) (push) Canceled after 0s
CI / Tests (Python 3.14) (push) Canceled after 0s
CI / Shell scripts (push) Canceled after 0s
2026-09-27 15:31:43 +09:00
viktor 440d8fc562 Проект переехал в Gitea StackWorks: обновления, установка одной командой и ISO берут код с git.stackworks.ru, запасной ZIP скачивается оттуда же, ноды со старым адресом GitHub переключаются сами 2026-09-27 15:31:43 +09:00
viktor 83511d6b09 История копии из Gitea (0.9.6 и разбор старого README) была присоединена, содержимое остаётся от GitHub-версии 0.11
CI / Tests (Python 3.11) (push) Canceled after 0s
CI / Tests (Python 3.12) (push) Canceled after 0s
CI / Tests (Python 3.13) (push) Canceled after 0s
CI / Tests (Python 3.14) (push) Canceled after 0s
CI / Shell scripts (push) Canceled after 0s
2026-09-27 14:47:52 +09:00
viktor 83300b6c5a Резервные копии: наблюдатель позднего выключения перепроверяет отмену после опроса машины и не запускает её, если «Выключить» нажали в этот момент 2026-09-26 09:34:57 +09:00
viktor 657d54fc75 Тесты vmops больше не зависят от свободного места во временной папке (на tmpfs падали 8 тестов) 2026-09-26 09:34:57 +09:00
viktor 5f2bdd921d docs: подробный README (разбор проекта при переносе в Gitea)
Старый README сохранён как README.upstream.md.
2026-08-20 04:18:36 +09:00
21 изменённых файлов: 330 добавлений и 52 удалений
+5 -2
Просмотреть файл
@@ -7,6 +7,9 @@ Virtuality — простая замена Proxmox для дома и малог
**Общайся с владельцем проекта по-русски.** Весь текст интерфейса — на простом русском, без жаргона
(термины вроде qcow2, libvirt, nvram — только в раскрывающихся блоках «Для специалистов»).
Репозиторий: https://git.stackworks.ru/viktor/virtuality (Gitea StackWorks). С GitHub проект переехал в сентябре 2026 года;
ноды со старым адресом переключаются сами (`update_core.migrate_remote`, `migrate_update_remote` в `scripts/update_target.sh`).
## Устройство репозитория
- `web/app.py` — основное приложение FastAPI (машины, ISO, образы дисков, сеть, журналы, обновления, консоль).
@@ -55,8 +58,8 @@ make iso # ISO в dist/ (нужны xorriso, git, curl, p
- Работаем прямо в `main` (так решил владелец). Перед каждым пушем — `make check`; не пушить
недоделанное: ноды на канале «Ранний доступ» (`main`) подтягивают main каждую ночь.
- Узлы на канале `stable` (по умолчанию) обновляются только до тегов `vX.Y.Z`. Релиз: поднять `VERSION`,
добавить запись в `updates/versions.json`, запушить, поставить тег `vX.Y.Z` на main — тег же запускает
сборку ISO в GitHub Actions (artifacts `virtuality-iso-amd64/arm64`).
добавить запись в `updates/versions.json`, запушить, поставить тег `vX.Y.Z` на main. Раннера Actions нет:
ISO собирается вручную `make iso`, CI заменяет `make check` перед пушем.
- Секреты и пароли не логировать; пароль cloud-init хранится только как SHA-512 crypt.
- Не возвращать удалённое: Cockpit, консольный dashboard, эмуляцию ARM64 на x86, PXE, смену
архитектуры машины, скрипты bootstrap/clean_install/setup_bridge_br0/create_test_vm.
+8 -9
Просмотреть файл
@@ -2,7 +2,7 @@
**Virtuality** — простая замена Proxmox для дома и малого офиса. Один сервер, виртуальные машины и понятная панель в браузере на русском языке. Внутри — проверенные KVM, QEMU и libvirt, снаружи — интерфейс, в котором не нужна командная строка.
Текущая версия: **0.11.0** (файл `VERSION`, журнал изменений — `updates/versions.json`).
Текущая версия: **0.11.1** (файл `VERSION`, журнал изменений — `updates/versions.json`).
Что умеет:
@@ -70,14 +70,13 @@
Где взять образ:
- **GitHub Actions.** Откройте вкладку Actions репозитория → workflow **Build ISO image** → последний успешный запуск → раздел Artifacts → `virtuality-iso-amd64` (или `virtuality-iso-arm64`). Внутри архива — `.iso` и файл `.sha256`. Артефакты хранятся 30 дней; новый образ собирается при каждом выпуске (тег `vX.Y.Z`), запустить сборку вручную можно кнопкой Run workflow.
- **Собрать самому** на любой Linux-машине (root не нужен):
```bash
sudo apt install -y xorriso git curl python3-pip
git clone https://github.com/viktor138irk/virtuality.git
git clone https://git.stackworks.ru/viktor/virtuality.git
cd virtuality
make iso # dist/virtuality-0.11.0-ubuntu-26.04-amd64.iso
make iso # dist/virtuality-0.11.1-ubuntu-26.04-amd64.iso
```
Дальше: записать образ на флешку (Rufus, balenaEtcher или `dd`), загрузиться с неё, выбрать **Установить Virtuality**, ответить на три экрана установщика и дождаться перезагрузки. Через минуту после неё панель уже открывается в браузере — адрес показан на экране сервера. Все подробности, параметры сборки и полностью автоматическая установка — в [docs/IMAGE.md](docs/IMAGE.md).
@@ -87,7 +86,7 @@
Подходит для уже установленного Ubuntu Server 24.04 / 26.04 или Debian 13. Работает и под `root`, и под обычным пользователем с `sudo`:
```bash
curl -fsSL https://raw.githubusercontent.com/viktor138irk/virtuality/main/install.sh | bash
curl -fsSL https://git.stackworks.ru/viktor/virtuality/raw/branch/main/install.sh | bash
```
Скрипт проверит требования, поставит пакеты KVM/QEMU/libvirt, команду `vhealth` и панель, а в конце покажет адрес панели. Логин — тот пользователь, от которого запущена установка (через `sudo` — ваш пользователь, под `root` — `root`); пароль — его пароль Linux. Если пароль ещё не задан: `sudo passwd <пользователь>`.
@@ -105,7 +104,7 @@ VIRTUALITY_INSTALL_VERBOSE=1 # показывать вывод команд вм
Пример:
```bash
curl -fsSL https://raw.githubusercontent.com/viktor138irk/virtuality/main/install.sh | VIRTUALITY_USER=admin bash
curl -fsSL https://git.stackworks.ru/viktor/virtuality/raw/branch/main/install.sh | VIRTUALITY_USER=admin bash
```
При первом входе в панель откроется мастер настройки — тот же, что после установки с ISO.
@@ -116,7 +115,7 @@ curl -fsSL https://raw.githubusercontent.com/viktor138irk/virtuality/main/instal
```bash
sudo mkdir -p /opt/virtuality
sudo git clone https://github.com/viktor138irk/virtuality.git /opt/virtuality/source
sudo git clone https://git.stackworks.ru/viktor/virtuality.git /opt/virtuality/source
cd /opt/virtuality/source
sudo bash install_virtuality_node.sh # пакеты KVM/QEMU/libvirt, пулы, firewall
sudo bash scripts/install_healthcheck_command.sh # команда sudo vhealth
@@ -265,7 +264,7 @@ sudo virtuality-ctl uninstall [--purge] [--yes] # удалить Virtuality
- **Из панели**: раздел **Обновления** → «Проверить сейчас» → «Обновить». Машины во время обновления продолжают работать. Что изменилось — видно на той же странице.
- **Автоматически**: таймер `virtuality-auto-update.timer` проверяет обновления каждую ночь около 4:00 по времени сервера. Включается и выключается в «Настройках» или командой `sudo virtuality-ctl set VIRTUALITY_AUTO_UPDATE=0`.
- **Каналы**: `stable` — только выпущенные версии (последний тег `vX.Y.Z`; пока тегов нет, стабильный канал следует ветке `main`), `main` — все изменения ветки `main`. Нода никогда не откатывается на более старую версию сама.
- **Вручную из терминала**: `sudo bash /opt/virtuality/source/scripts/apply_github_update.sh` — тот же сценарий, что и в панели (git fetch, при недоступности git — ZIP с GitHub, затем `scripts/install_web_panel.sh` и перезапуск панели). Журнал: `/var/log/virtuality/update.log`.
- **Вручную из терминала**: `sudo bash /opt/virtuality/source/scripts/apply_github_update.sh` — тот же сценарий, что и в панели (git fetch, при недоступности git — ZIP-архив с git.stackworks.ru, затем `scripts/install_web_panel.sh` и перезапуск панели). Журнал: `/var/log/virtuality/update.log`.
- **Откат**: перед каждым обновлением предыдущая версия панели сохраняется в `/opt/virtuality/web.prev`. Если новая версия не запустилась: `sudo virtuality-ctl rollback`.
---
@@ -353,7 +352,7 @@ VIRTUALITY_DEV_SETUP=finished python3 tests/dev_server.py # настроен
Устройство кода описано в [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md). Интерфейс: шаблоны `web/templates` (каркас `base.html`, компоненты `_ui.html`), стили `web/static/app.css`, поведение `web/static/panel.js`; шрифт и иконки лежат в `web/static`. Скриншоты страниц: `node tests/screenshots.mjs` (нужен Playwright).
GitHub Actions: `ci.yml` запускает pyflakes, shellcheck и тесты на Python 3.11–3.14; `image.yml` собирает ISO по тегу `v*` и вручную. Ветка `main` — рабочая ветка нод на канале `main`; выпуск для стабильного канала — тег `vX.Y.Z`.
Репозиторий живёт в Gitea StackWorks: https://git.stackworks.ru/viktor/virtuality (до сентября 2026 года был на GitHub; ноды со старым адресом переключаются на новый сами при первой проверке обновлений). Ветка `main` — рабочая ветка нод на канале `main`; выпуск для стабильного канала — тег `vX.Y.Z`. Раннер Actions не подключён: перед каждым пушем запускается `make check`, ISO собирается вручную `make iso`. Файлы `.github/workflows` (`ci.yml` — проверки на Python 3.11–3.14, `image.yml` — сборка ISO) Gitea тоже понимает, они заработают, если раннер появится.
---
+1 -1
Просмотреть файл
@@ -1 +1 @@
0.11.0
0.11.1
+4 -2
Просмотреть файл
@@ -146,9 +146,11 @@ libvirt-сеть с мостом `virbr100`, подсеть `192.168.100.0/24`,
## Обновления
Ноды обновляются из `origin` своего `/opt/virtuality/source` — по умолчанию https://git.stackworks.ru/viktor/virtuality.git. Проект переехал туда с GitHub в сентябре 2026 года; ноду со старым адресом GitHub `update_core.migrate_remote()` и `migrate_update_remote` в `scripts/update_target.sh` переключают на новый перед `git fetch`.
1. **Канал** (`VIRTUALITY_UPDATE_CHANNEL` в `web.env`): `stable` — последний тег `vX.Y.Z` (пока тегов нет — ветка `main`); `main` — ветка `main`. Логика одна и та же в `web/update_core.py` и `scripts/update_target.sh`.
2. **Проверка** (`/update/check`, ночной `scripts/auto_update_check.sh`): `git fetch --tags`, сравнение текущего коммита с целевым; обновление предлагается, только если целевой коммит — потомок текущего (нода никогда не откатывается сама). Список изменений берётся из `updates/versions.json` целевой версии.
3. **Применение** (`scripts/apply_github_update.sh`, запускается через `systemd-run`): `git checkout -B main <цель>` и `git clean`; если git недоступен — скачивание ZIP-архива с GitHub и `rsync`; затем `scripts/install_web_panel.sh` (панель копируется в `web.new`, старая переименовывается в `web.prev`, venv обновляется) и `systemctl restart virtuality-web`. Состояние — `/var/lib/virtuality/update/state.json`, журнал — `/var/log/virtuality/update.log`.
3. **Применение** (`scripts/apply_github_update.sh`, запускается через `systemd-run`): `git checkout -B main <цель>` и `git clean`; если git недоступен — скачивание ZIP-архива того же репозитория (`<адрес>/archive/<ветка или тег>.zip`, так отдают и Gitea, и GitHub) и `rsync`; затем `scripts/install_web_panel.sh` (панель копируется в `web.new`, старая переименовывается в `web.prev`, venv обновляется) и `systemctl restart virtuality-web`. Состояние — `/var/lib/virtuality/update/state.json`, журнал — `/var/log/virtuality/update.log`.
4. **Откат**: `virtuality-ctl rollback` меняет местами `web` и `web.prev`.
Установщики и скрипты вне `web/`:
@@ -169,4 +171,4 @@ image/build-iso.sh, autoinstall.yaml, files/, grub/ сборка ISO
## Тесты и CI
`tests/conftest.py` подменяет `run_cmd`, так что `virsh`, `systemctl`, `qemu-img` и остальные команды имитируются — тесты идут на любой машине без libvirt. `tests/dev_server.py` поднимает демо-панель с теми же подменами. `make check` = pyflakes + shellcheck + pytest; `.github/workflows/ci.yml` запускает это на Python 3.11–3.14, `.github/workflows/image.yml` собирает ISO.
`tests/conftest.py` подменяет `run_cmd`, так что `virsh`, `systemctl`, `qemu-img` и остальные команды имитируются — тесты идут на любой машине без libvirt. `tests/dev_server.py` поднимает демо-панель с теми же подменами. `make check` = pyflakes + shellcheck + pytest, запускается перед каждым пушем. `.github/workflows/ci.yml` (то же на Python 3.11–3.14) и `.github/workflows/image.yml` (сборка ISO) заработают в Gitea Actions, когда будет подключён раннер.
+12 -16
Просмотреть файл
@@ -20,13 +20,7 @@
## Где взять готовый образ
Workflow `.github/workflows/image.yml` собирает ISO в GitHub Actions:
- автоматически при выпуске — пуш тега `v*` (amd64 и arm64);
- при pull request, который меняет `image/**`, `scripts/virtuality_firstboot.sh`, `web/requirements.txt` или сам workflow;
- вручную: Actions → **Build ISO image** → Run workflow → архитектура `amd64`, `arm64` или `both`.
После сборки workflow проверяет, что в образе есть загрузочная запись UEFI, `autoinstall.yaml` разбирается как YAML, а в `grub.cfg` есть пункт Virtuality. Образы Ubuntu Server больше лимита GitHub Releases (2 ГБ на файл), поэтому ISO публикуются как **artifacts** запуска (`virtuality-iso-amd64`, `virtuality-iso-arm64`) и хранятся 30 дней. В артефакте — `.iso` и `.sha256`.
Образ собирается вручную командой `make iso` (см. ниже): раннер Actions в Gitea не подключён. Workflow `.github/workflows/image.yml` остаётся в репозитории и заработает, как только раннер появится: он собирает ISO по тегу `v*` и вручную, проверяет загрузочную запись UEFI, `autoinstall.yaml` и пункт Virtuality в `grub.cfg` и сохраняет `.iso` и `.sha256` как artifacts запуска.
---
@@ -41,7 +35,7 @@ sudo apt install -y xorriso git curl python3-pip
Сборка:
```bash
git clone https://github.com/viktor138irk/virtuality.git
git clone https://git.stackworks.ru/viktor/virtuality.git
cd virtuality
make iso # amd64
make iso ARCH=arm64 # ARM64-серверы с UEFI
@@ -50,8 +44,8 @@ make iso ARCH=arm64 # ARM64-серверы с UEFI
`image/build-iso.sh` сам находит последний Ubuntu 26.04.x live-server ISO, скачивает его (около 3 ГБ), проверяет SHA256 и кладёт результат в `dist/`:
```text
dist/virtuality-0.11.0-ubuntu-26.04-amd64.iso
dist/virtuality-0.11.0-ubuntu-26.04-amd64.iso.sha256
dist/virtuality-0.11.1-ubuntu-26.04-amd64.iso
dist/virtuality-0.11.1-ubuntu-26.04-amd64.iso.sha256
```
Скачанный ISO Ubuntu кешируется в `.cache/iso` (или в `VIRTUALITY_ISO_CACHE`). Уже скачанный образ можно передать явно:
@@ -104,16 +98,18 @@ make iso ISO_ARGS="--web-port 8089 --no-auto-update --timezone Europe/Moscow"
Для массовой раскатки:
```bash
make iso ISO_ARGS="--unattended --username admin --password-hash '$(openssl passwd -6)' --hostname node01"
make iso ISO_ARGS="--unattended --username sysadmin --password-hash '$(openssl passwd -6)' --hostname node01"
```
Системные имена вроде `admin`, `root` или `kvm` установщик Ubuntu не принимает, и установка остановится на первом шаге. Поэтому `build-iso.sh` сразу отказывается собирать образ с таким `--username`. Полный список лежит в `image/reserved-usernames`, это копия списка из установщика Ubuntu.
> ⚠️ Такой образ сам уходит в установку через 10 секунд в меню и **без подтверждения стирает самый большой диск** машины. Файл получает суффикс `-unattended`, пункты меню — пометку «автоматически, диск будет стёрт». Не оставляйте такую флешку в серверах.
---
## Что спрашивает установщик
Меню загрузки: **Установить Virtuality 0.11.0** (выбирается сам через 10 секунд), тот же пункт с новым ядром HWE, загрузка с другого диска, настройки UEFI, проверка памяти.
Меню загрузки: **Установить Virtuality 0.11.1** (выбирается сам через 10 секунд), тот же пункт с новым ядром HWE, загрузка с другого диска, настройки UEFI, проверка памяти.
Сценарий `image/autoinstall.yaml` оставляет интерактивными только три экрана установщика Ubuntu:
@@ -165,14 +161,14 @@ cat /var/log/virtuality/firstboot.log
Сверьте контрольную сумму скачанного или собранного образа:
```bash
sha256sum -c virtuality-0.11.0-ubuntu-26.04-amd64.iso.sha256
sha256sum -c virtuality-0.11.1-ubuntu-26.04-amd64.iso.sha256
```
Убедиться, что образ загрузочный и содержит нужные файлы (то же делает CI):
```bash
xorriso -indev virtuality-0.11.0-ubuntu-26.04-amd64.iso -report_el_torito plain 2>/dev/null | grep UEFI
xorriso -osirrox on -indev virtuality-0.11.0-ubuntu-26.04-amd64.iso -extract /autoinstall.yaml autoinstall.yaml
xorriso -indev virtuality-0.11.1-ubuntu-26.04-amd64.iso -report_el_torito plain 2>/dev/null | grep UEFI
xorriso -osirrox on -indev virtuality-0.11.1-ubuntu-26.04-amd64.iso -extract /autoinstall.yaml autoinstall.yaml
```
---
@@ -186,7 +182,7 @@ xorriso -osirrox on -indev virtuality-0.11.0-ubuntu-26.04-amd64.iso -extract /au
- **Linux**: balenaEtcher или `dd`, где `/dev/sdX` — устройство флешки (проверьте через `lsblk`):
```bash
sudo dd if=dist/virtuality-0.11.0-ubuntu-26.04-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
sudo dd if=dist/virtuality-0.11.1-ubuntu-26.04-amd64.iso of=/dev/sdX bs=4M status=progress conv=fsync
```
Загрузитесь с флешки (меню загрузки — обычно F8, F11, F12 или Esc при включении), выберите **Установить Virtuality**.
+1 -1
Просмотреть файл
@@ -59,7 +59,7 @@ VIRTUALITY_SKIP_REQUIREMENTS=1 # отключить проверки совс
Например, для тестового стенда с маленьким диском:
```bash
curl -fsSL https://raw.githubusercontent.com/viktor138irk/virtuality/main/install.sh | VIRTUALITY_MIN_VAR_FREE_MB=8192 bash
curl -fsSL https://git.stackworks.ru/viktor/virtuality/raw/branch/main/install.sh | VIRTUALITY_MIN_VAR_FREE_MB=8192 bash
```
Установка с ISO эти пороги не применяет: диск и память проверяются мастером настройки, который покажет предупреждение, но не остановит установку.
+1 -1
Просмотреть файл
@@ -6,7 +6,7 @@ Virtuality — панель для одного домашнего сервер
**Установка и настройка**
- Установочный ISO на базе Ubuntu Server 26.04 LTS (amd64, arm64), сборка `make iso` и в GitHub Actions, полностью автоматический режим.
- Установочный ISO на базе Ubuntu Server 26.04 LTS (amd64, arm64), сборка `make iso`, полностью автоматический режим.
- Первая загрузка: панель ставится первой и показывает ход установки в браузере, при ошибке продолжает сама.
- Мастер настройки: часовой пояс, пароль, HTTPS, сеть машин (NAT или домашняя сеть с автооткатом), отдельный диск под машины, обновления.
- Страница «Настройки», команда `virtuality-ctl`, установка одной командой на Ubuntu 24.04 / 26.04 и Debian 13.
+3 -2
Просмотреть файл
@@ -21,7 +21,7 @@ UBUNTU_ISO=""
CACHE_DIR="${VIRTUALITY_ISO_CACHE:-${REPO_ROOT}/.cache/iso}"
OUTPUT_DIR="${REPO_ROOT}/dist"
GIT_REF="HEAD"
REPO_URL="https://github.com/viktor138irk/virtuality.git"
REPO_URL="https://git.stackworks.ru/viktor/virtuality.git"
WEB_PORT="8088"
AUTO_UPDATE="1"
UNATTENDED="0"
@@ -47,7 +47,7 @@ Options:
--ref GIT_REF Git ref of Virtuality to bundle (default: ${GIT_REF})
--repo-url URL Update remote configured on installed nodes (default: ${REPO_URL})
--web-port PORT Web panel port on installed nodes (default: ${WEB_PORT})
--no-auto-update Disable the nightly GitHub auto-update on installed nodes
--no-auto-update Disable the nightly auto-update on installed nodes
--locale LOCALE Installer/system locale (default: ${LOCALE})
--keyboard LAYOUT Keyboard layout (default: ${KEYBOARD})
--timezone TZ System timezone (default: ${TIMEZONE} — detected by the installer; the wizard can change it)
@@ -106,6 +106,7 @@ esac
[[ "$WEB_PORT" =~ ^[0-9]+$ ]] && (( WEB_PORT >= 1 && WEB_PORT <= 65535 )) || die "invalid --web-port: $WEB_PORT"
if [[ "$UNATTENDED" == "1" ]]; then
[[ "$USERNAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || die "--unattended needs a valid --username"
! grep -qxF "$USERNAME" "${IMAGE_DIR}/reserved-usernames" || die "--username ${USERNAME} is reserved by the Ubuntu installer (like admin, root, kvm); pick another name"
[[ "$PASSWORD_HASH" == \$* ]] || die "--unattended needs --password-hash (generate: openssl passwd -6)"
[[ "$HOSTNAME_VALUE" =~ ^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}$ ]] || die "invalid --hostname"
fi
+1 -1
Просмотреть файл
@@ -1,6 +1,6 @@
[Unit]
Description=Virtuality first boot setup
Documentation=https://github.com/viktor138irk/virtuality/blob/main/docs/IMAGE.md
Documentation=https://git.stackworks.ru/viktor/virtuality/src/branch/main/docs/IMAGE.md
Wants=network-online.target
After=network-online.target
ConditionPathExists=!/var/lib/virtuality/.firstboot-done
+127
Просмотреть файл
@@ -0,0 +1,127 @@
# Имена, которые установщик Ubuntu (subiquity) не даёт выбрать для первого пользователя.
# Копия https://github.com/canonical/subiquity/blob/main/reserved-usernames (27.09.2026).
# build-iso.sh проверяет по ней --username, иначе автоматическая установка падает на первом шаге.
# Static users from base-passwd/passwd.master (3.5.41).
root
daemon
bin
sys
sync
games
man
lp
mail
news
uucp
proxy
www-data
backup
list
irc
gnats
nobody
# Other static groups from base-passwd/group.master (3.5.41).
adm
tty
disk
kmem
dialout
fax
voice
cdrom
floppy
tape
sudo
audio
dip
operator
src
shadow
utmp
video
sasl
plugdev
staff
users
nogroup
# Reserved usernames listed in base-passwd/README (3.5.41).
netplan
ftn
mysql
tac-plus
alias
qmail
qmaild
qmails
qmailr
qmailq
qmaill
qmailp
asterisk
vpopmail
vchkpw
slurm
hacluster
haclient
grsec-tpe
grsec-sock-all
grsec-sock-clt
grsec-sock-srv
grsec-proc
ceph
opensrf
libvirt-qemu
# Ubuntu creates the admin group and adds the first user to it in order to
# grant them sudo privileges.
admin
# Other miscellaneous system users/groups created by common packages. While
# it's useful to add things here that people might run into, it's not
# absolutely critical; the worst that will happen is that the installation
# will fail at some later point.
Debian-exim
bind
crontab
cupsys
dcc
dhcp
dictd
dnsmasq
dovecot
fetchmail
firebird
ftp
fuse
gdm
haldaemon
hplilp
identd
input
jwhois
klog
kvm
lpadmin
maas
messagebus
mythtv
netdev
powerdev
radvd
render
saned
sbuild
scanner
sgx
slocate
ssh
sshd
ssl-cert
sslwrap
statd
syslog
telnetd
tftpd
+2 -2
Просмотреть файл
@@ -6,7 +6,7 @@ set -euo pipefail
# Public install entrypoint for clean Ubuntu/Debian servers.
# ==========================================================
SELF_INSTALL_URL="${VIRTUALITY_INSTALL_URL:-https://raw.githubusercontent.com/viktor138irk/virtuality/main/install.sh}"
SELF_INSTALL_URL="${VIRTUALITY_INSTALL_URL:-https://git.stackworks.ru/viktor/virtuality/raw/branch/main/install.sh}"
if [[ "${EUID}" -ne 0 ]]; then
if command -v sudo >/dev/null 2>&1; then
@@ -27,7 +27,7 @@ if [[ "${EUID}" -ne 0 ]]; then
exit 1
fi
REPO_URL="${VIRTUALITY_REPO_URL:-https://github.com/viktor138irk/virtuality.git}"
REPO_URL="${VIRTUALITY_REPO_URL:-https://git.stackworks.ru/viktor/virtuality.git}"
DEFAULT_INSTALL_USER="${SUDO_USER:-root}"
INSTALL_USER="${VIRTUALITY_USER:-$DEFAULT_INSTALL_USER}"
PROJECT_BASE_DIR="${VIRTUALITY_PROJECT_BASE_DIR:-/opt/virtuality}"
+9 -8
Просмотреть файл
@@ -10,11 +10,11 @@ BRANCH="${VIRTUALITY_UPDATE_BRANCH:-main}"
TMP_DIR="${STATE_DIR}/tmp"
# shellcheck source=scripts/update_target.sh
. "$(dirname "${BASH_SOURCE[0]}")/update_target.sh"
mkdir -p "$STATE_DIR" "$TMP_DIR" "$(dirname "$LOG_FILE")"
MOVED_REMOTE="$(migrate_update_remote)"
resolve_update_target
REPO_ZIP_URL="${VIRTUALITY_UPDATE_ZIP_URL:-$TARGET_ZIP_URL}"
mkdir -p "$STATE_DIR" "$TMP_DIR" "$(dirname "$LOG_FILE")"
now() { date '+%Y-%m-%d %H:%M:%S'; }
json_escape() { python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1])'; }
write_state() {
@@ -58,7 +58,7 @@ fetch_zip_update() {
log "Fallback update via ZIP started"
log "ZIP URL: $REPO_ZIP_URL"
write_state "running" "GitHub git fetch не сработал, пробуем ZIP fallback"
write_state "running" "git fetch не сработал, пробуем скачать ZIP-архив"
if command -v curl >/dev/null 2>&1; then
if ! try_run curl -L --connect-timeout 20 --max-time 240 --retry 3 --retry-delay 3 -o "$zip_file" "$REPO_ZIP_URL"; then
@@ -111,7 +111,8 @@ fetch_zip_update() {
STARTED_AT="$(now)"
write_state "running" "Обновление запущено"
log "============================================================"
log "Virtuality GitHub update started"
log "Virtuality update started"
[ -n "$MOVED_REMOTE" ] && log "$MOVED_REMOTE"
log "source: $SOURCE_DIR"
log "remote: $REMOTE"
log "branch: $BRANCH"
@@ -129,7 +130,7 @@ log "before version: $BEFORE_VERSION"
GIT_UPDATED=0
if [ -d "$SOURCE_DIR/.git" ]; then
cd "$SOURCE_DIR" || exit 1
write_state "running" "Получаем изменения из GitHub через git"
write_state "running" "Получаем изменения через git"
if try_run git fetch --tags --force "$REMOTE" "$BRANCH"; then
resolve_update_target
log "channel: ${UPDATE_CHANNEL}, target: ${TARGET_LABEL}"
@@ -140,7 +141,7 @@ if [ -d "$SOURCE_DIR/.git" ]; then
log "Local changes before update:"
printf '%s\n' "$LOCAL_STATUS" >> "$LOG_FILE"
fi
write_state "running" "Синхронизируем исходники с GitHub через git reset"
write_state "running" "Синхронизируем исходники через git reset"
log "Deploy mode: git checkout -B ${BRANCH} ${TARGET_LABEL}"
if try_run git checkout -q -f -B "$BRANCH" "$TARGET_COMMIT" && try_run git clean -fd; then
GIT_UPDATED=1
@@ -159,7 +160,7 @@ fi
if [ "$GIT_UPDATED" != "1" ]; then
if ! fetch_zip_update; then
write_state "error" "Не удалось обновиться ни через git, ни через ZIP fallback. Проверь доступ к github.com, curl/wget/unzip и DNS."
write_state "error" "Не удалось обновиться ни через git, ни через ZIP fallback. Проверьте доступ сервера к ${REPO_ZIP_URL%%/archive/*}, curl/wget/unzip и DNS."
exit 1
fi
fi
@@ -180,6 +181,6 @@ run_step bash scripts/install_web_panel.sh
write_state "running" "Перезапускаем virtuality-web"
run_step systemctl restart virtuality-web
log "Virtuality GitHub update finished successfully"
log "Virtuality update finished successfully"
write_state "success" "Обновление завершено: ${BEFORE_VERSION} → ${AFTER_VERSION}"
exit 0
+9 -2
Просмотреть файл
@@ -35,10 +35,14 @@ JSON
}
diag_network() {
local web repo_host
web="$(repo_web_url)"
repo_host="${web#https://}"
repo_host="${repo_host%%/*}"
log "diagnostics: host=$(hostname 2>/dev/null || echo unknown)"
log "diagnostics: date=$(date -Is 2>/dev/null || date)"
if command -v getent >/dev/null 2>&1; then
getent hosts github.com >> "$LOG_FILE" 2>&1 || log "diagnostics: DNS github.com failed"
getent hosts "$repo_host" >> "$LOG_FILE" 2>&1 || log "diagnostics: DNS $repo_host failed"
fi
if command -v git >/dev/null 2>&1; then
git --version >> "$LOG_FILE" 2>&1 || true
@@ -47,7 +51,7 @@ diag_network() {
log "diagnostics: git not found"
fi
if command -v curl >/dev/null 2>&1; then
curl -I -L --connect-timeout 10 --max-time 25 https://github.com/ >> "$LOG_FILE" 2>&1 || log "diagnostics: curl github.com failed"
curl -I -L --connect-timeout 10 --max-time 25 "$web" >> "$LOG_FILE" 2>&1 || log "diagnostics: curl $repo_host failed"
else
log "diagnostics: curl not found"
fi
@@ -89,6 +93,9 @@ diag_network() {
current="unknown"
fi
moved="$(migrate_update_remote)"
[ -n "$moved" ] && log "$moved"
if [ -d "$SOURCE_DIR/.git" ]; then
if git fetch --quiet --tags --force "$REMOTE" "$BRANCH" >> "$LOG_FILE" 2>&1; then
resolve_update_target
+33 -3
Просмотреть файл
@@ -4,6 +4,10 @@
# main — every change on the main branch
# Needs SOURCE_DIR, REMOTE and BRANCH; sets UPDATE_CHANNEL, TARGET_REF, TARGET_LABEL, TARGET_ZIP_URL.
# Where the project lives. Nodes installed before the move from GitHub are switched over by migrate_update_remote.
DEFAULT_REPO_URL="https://git.stackworks.ru/viktor/virtuality.git"
LEGACY_REPO_URLS=("https://github.com/viktor138irk/virtuality.git" "https://github.com/viktor138irk/virtuality")
update_channel() {
local value="${VIRTUALITY_UPDATE_CHANNEL:-}"
if [[ -z "$value" && -f /var/lib/virtuality/config/web.env ]]; then
@@ -12,18 +16,44 @@ update_channel() {
[[ "$value" == "main" ]] && echo main || echo stable
}
remote_url() {
git -C "$SOURCE_DIR" remote get-url "$REMOTE" 2>/dev/null || true
}
migrate_update_remote() {
[[ -d "${SOURCE_DIR}/.git" ]] || return 0
local url legacy
url="$(remote_url)"
for legacy in "${LEGACY_REPO_URLS[@]}"; do
if [[ "$url" == "$legacy" ]]; then
git -C "$SOURCE_DIR" remote set-url "$REMOTE" "$DEFAULT_REPO_URL" && echo "update remote moved: $url -> $DEFAULT_REPO_URL"
return 0
fi
done
}
# Web address of the repository for ZIP downloads: the remote itself when it is https, otherwise the default.
# Gitea and GitHub both serve <repo>/archive/<branch or tag>.zip.
repo_web_url() {
local url=""
[[ -d "${SOURCE_DIR}/.git" ]] && url="$(remote_url)"
[[ "$url" == https://* ]] || url="$DEFAULT_REPO_URL"
echo "${url%.git}"
}
resolve_update_target() {
local tag=""
local tag="" web
web="$(repo_web_url)"
UPDATE_CHANNEL="$(update_channel)"
TARGET_REF="${REMOTE}/${BRANCH}"
TARGET_LABEL="${BRANCH}"
TARGET_ZIP_URL="https://github.com/viktor138irk/virtuality/archive/refs/heads/${BRANCH}.zip"
TARGET_ZIP_URL="${web}/archive/${BRANCH}.zip"
if [[ "$UPDATE_CHANNEL" == "stable" && -d "${SOURCE_DIR}/.git" ]]; then
tag="$(git -C "$SOURCE_DIR" tag --list 'v[0-9]*' --sort=-v:refname | grep -E '^v[0-9]+(\.[0-9]+)*$' | head -n1 || true)"
if [[ -n "$tag" ]]; then
TARGET_REF="refs/tags/${tag}"
TARGET_LABEL="$tag"
TARGET_ZIP_URL="https://github.com/viktor138irk/virtuality/archive/refs/tags/${tag}.zip"
TARGET_ZIP_URL="${web}/archive/${tag}.zip"
fi
fi
}
+17
Просмотреть файл
@@ -643,3 +643,20 @@ def test_late_restart_watch_is_cancelled_by_user_action(fast_worker, monkeypatch
watchers[0].join(timeout=5)
assert not watchers[0].is_alive() and ["virsh", "start", "web01"] not in virsh.calls
assert "Наблюдение снято" in core.read_operation(operation["id"])["log_tail"]
def test_late_restart_rechecks_cancel_after_shutdown(fast_worker, monkeypatch):
virsh = FakeVirsh(running=True, shuts_down=False)
monkeypatch.setattr(core, "run_cmd", virsh)
monkeypatch.setattr(backups, "LATE_SHUTDOWN_WATCH", 5.0)
operation = core.new_operation("backup", "Резервная копия web01", vm_name="web01")
def shut_down_by_user(vm):
backups.cancel_late_restart(vm) # «Выключить» нажали, пока наблюдатель спрашивал состояние машины
return False
monkeypatch.setattr(core, "vm_is_running", shut_down_by_user)
backups.restart_after_late_shutdown(operation, "web01")
assert ["virsh", "start", "web01"] not in virsh.calls
assert "Наблюдение снято" in core.read_operation(operation["id"])["log_tail"]
assert "web01" not in backups.LATE_WATCHES
+64
Просмотреть файл
@@ -4,6 +4,7 @@ from pathlib import Path
import pytest
import network_core
from conftest import ROOT
import restore_network
import update_core
@@ -104,6 +105,59 @@ def test_update_not_offered_when_local_is_newer(tmp_path, monkeypatch, data_dirs
assert update_core.check_updates(fetch=True)["has_update"] is True
GITEA = "https://git.stackworks.ru/viktor/virtuality"
def make_source(path):
import subprocess
def git(*args):
return subprocess.run(["git", "-c", "user.name=t", "-c", "user.email=t@t", *args], cwd=path, check=True, capture_output=True, text=True).stdout.strip()
path.mkdir()
git("init", "-q", "-b", "main")
git("commit", "-q", "--allow-empty", "-m", "base")
git("remote", "add", "origin", "https://github.com/viktor138irk/virtuality.git")
return git
def test_migrate_remote_moves_only_the_old_github_address(tmp_path, monkeypatch, data_dirs):
git = make_source(tmp_path / "source")
monkeypatch.setattr(update_core, "SOURCE_DIR", tmp_path / "source")
assert update_core.migrate_remote() is True
assert git("remote", "get-url", "origin") == GITEA + ".git"
assert update_core.migrate_remote() is False
git("remote", "set-url", "origin", "https://example.org/me/fork.git")
assert update_core.migrate_remote() is False
assert git("remote", "get-url", "origin") == "https://example.org/me/fork.git"
def test_update_target_script_downloads_from_gitea(tmp_path):
import os
import subprocess
source = tmp_path / "source"
git = make_source(source)
def target(channel, script="resolve_update_target"):
env = {**os.environ, "VIRTUALITY_UPDATE_CHANNEL": channel}
code = f'SOURCE_DIR="{source}"; REMOTE=origin; BRANCH=main; . "{ROOT}/scripts/update_target.sh"; {script}; echo "$TARGET_LABEL $TARGET_ZIP_URL"'
return subprocess.run(["bash", "-c", code], env=env, check=True, capture_output=True, text=True).stdout.strip().splitlines()
moved, result = target("main", "migrate_update_remote; resolve_update_target")
assert moved == f"update remote moved: https://github.com/viktor138irk/virtuality.git -> {GITEA}.git"
assert result == f"main {GITEA}/archive/main.zip"
assert git("remote", "get-url", "origin") == GITEA + ".git"
git("tag", "v0.11.1")
assert target("stable") == [f"v0.11.1 {GITEA}/archive/v0.11.1.zip"]
assert target("main") == [f"main {GITEA}/archive/main.zip"]
# A fork downloads from itself; a non-https remote falls back to the project's address.
git("remote", "set-url", "origin", "https://example.org/me/fork.git")
assert target("stable", "migrate_update_remote; resolve_update_target") == ["v0.11.1 https://example.org/me/fork/archive/v0.11.1.zip"]
git("remote", "set-url", "origin", "git@example.org:me/fork.git")
assert target("stable") == [f"v0.11.1 {GITEA}/archive/v0.11.1.zip"]
def test_stable_channel_follows_release_tags(tmp_path, monkeypatch, data_dirs):
import subprocess
@@ -140,3 +194,13 @@ def test_stable_channel_follows_release_tags(tmp_path, monkeypatch, data_dirs):
(tmp_path / "web.env").write_text("VIRTUALITY_UPDATE_CHANNEL=main\n")
data = update_core.check_updates(fetch=True)
assert data["channel"] == "main" and data["target"] == "main" and data["has_update"]
@pytest.mark.parametrize("username", ["admin", "root", "kvm", "libvirt-qemu"])
def test_build_iso_refuses_names_the_ubuntu_installer_rejects(username):
"""Установщик Ubuntu не принимает системные имена: автоматический образ с таким именем падал на первом шаге."""
import subprocess
result = subprocess.run(["bash", str(ROOT / "image" / "build-iso.sh"), "--unattended", "--username", username, "--password-hash", "$6$salt$hash"], capture_output=True, text=True)
assert result.returncode != 0 and "reserved by the Ubuntu installer" in result.stderr
+5 -1
Просмотреть файл
@@ -35,13 +35,17 @@ class Recorder:
@pytest.fixture()
def record(data_dirs, monkeypatch):
"""Подменяет run_cmd и в core (vmops), и в app (обзор, страница машины)."""
"""Подменяет run_cmd и в core (vmops), и в app (обзор, страница машины).
Свободное место тоже подменено: временная папка тестов бывает маленькой (tmpfs),
а тесты про нехватку места задают его сами."""
import app
recorder = Recorder()
for module in (core, app):
monkeypatch.setattr(module, "run_cmd", recorder)
monkeypatch.setattr(vmops, "_samples", {})
monkeypatch.setattr(vmops, "free_space", lambda path: 500 * GIB)
return recorder
+13
Просмотреть файл
@@ -1,5 +1,18 @@
{
"versions": [
{
"version": "0.11.1",
"date": "2026-09-27",
"title": "Переезд обновлений на git.stackworks.ru",
"summary": "Проект переехал с GitHub на собственный сервер git.stackworks.ru, и обновления теперь приходят оттуда. Сервер, который до сих пор получал обновления с GitHub, переключится на новый адрес сам при следующей проверке. Заодно исправлена редкая ошибка резервных копий: машина могла снова запуститься, хотя её только что выключили вручную.",
"features": [
"Обновления, установка одной командой и установочный ISO берут код с git.stackworks.ru",
"Сервер со старым адресом GitHub сам переключается на новый при проверке обновлений",
"Запасное обновление архивом ZIP скачивается с того же адреса, откуда сервер получает обновления",
"Резервные копии: если машину выключили вручную, пока панель ждала её выключения, панель больше не запускает её снова",
"Версия обновлена до 0.11.1"
]
},
{
"version": "0.11.0",
"date": "2026-09-25",
+3
Просмотреть файл
@@ -473,6 +473,9 @@ def restart_after_late_shutdown(operation: dict[str, Any], vm: str) -> None:
core.append_operation_log(operation["id"], "Наблюдение снято: машиной занялись вручную или новой задачей.")
return
if not core.vm_is_running(vm):
if cancelled.is_set(): # «Выключить» нажали, пока virsh отвечал на вопрос о состоянии
core.append_operation_log(operation["id"], "Наблюдение снято: машиной занялись вручную или новой задачей.")
return
if any(other["id"] != operation["id"] for other in core.active_operations_for(vm, OPERATION_TYPES)):
core.append_operation_log(operation["id"], "Машина выключилась, но с ней уже работает другая задача — не запускаем.")
return
+1 -1
Просмотреть файл
@@ -41,7 +41,7 @@
</article>
<div class="alert info" data-update-running hidden>{{ icon('refresh-cw') }}<div><strong>Идёт обновление</strong><span data-update-state></span></div></div>
{% if not info.fetch_ok %}{{ alert('warning', 'Не удалось связаться с GitHub: ' ~ (info.fetch_error or 'нет ответа') ~ '. Проверьте доступ сервера в интернет.', 'Проверка не удалась') }}{% endif %}
{% if not info.fetch_ok %}{{ alert('warning', 'Не удалось связаться с сервером обновлений: ' ~ (info.fetch_error or 'нет ответа') ~ '. Проверьте доступ сервера в интернет.', 'Проверка не удалась') }}{% endif %}
<article class="card">
<div class="card-header"><div><h2>Что нового</h2><p>{{ 'Изменения, которые вы получите' if info.missing_versions else 'Последние изменения' }}</p></div></div>
+11
Просмотреть файл
@@ -14,6 +14,9 @@ DEFAULT_BRANCH = os.environ.get('VIRTUALITY_UPDATE_BRANCH', 'main')
REMOTE = os.environ.get('VIRTUALITY_UPDATE_REMOTE', 'origin')
NODE_ENV = Path(os.environ.get('VIRTUALITY_NODE_ENV', '/var/lib/virtuality/config/web.env'))
CHANNELS = {'stable': 'Стабильный — выпущенные версии', 'main': 'Ранний доступ — все изменения'}
# Проект переехал с GitHub в Gitea StackWorks; ноды со старым адресом переключаются сами (как и scripts/update_target.sh).
DEFAULT_REPO_URL = 'https://git.stackworks.ru/viktor/virtuality.git'
LEGACY_REPO_URLS = ('https://github.com/viktor138irk/virtuality.git', 'https://github.com/viktor138irk/virtuality')
class UpdateError(Exception):
@@ -193,6 +196,13 @@ def git_log_between(base: str, head: str, limit: int = 50) -> list[dict[str, str
return rows
def migrate_remote() -> bool:
result = run_cmd(['git', 'remote', 'get-url', REMOTE], cwd=SOURCE_DIR)
if not result['ok'] or result['stdout'] not in LEGACY_REPO_URLS:
return False
return run_cmd(['git', 'remote', 'set-url', REMOTE, DEFAULT_REPO_URL], cwd=SOURCE_DIR)['ok']
def check_updates(fetch: bool = True) -> dict[str, Any]:
ensure_state_dir()
if not source_ready():
@@ -200,6 +210,7 @@ def check_updates(fetch: bool = True) -> dict[str, Any]:
fetch_result = {'ok': True, 'stdout': '', 'stderr': '', 'cmd': 'skip'}
if fetch:
migrate_remote()
fetch_result = run_cmd(['git', 'fetch', '--quiet', '--tags', '--force', REMOTE, DEFAULT_BRANCH], cwd=SOURCE_DIR, timeout=60)
channel = update_channel()