Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
13 KiB
Перенос данных на боевой сервер (test → prod)
Мануал для случая: на тестовом сервере уже заполнены сотрудники, часть аптек и справочники; боевой сервер — новый; чат (Mattermost) на проде будет новый. Цель — перенести доменные данные, не заполняя всё заново, и корректно обойти шифрование, 2FA и привязки к Mattermost.
⚠️ Сначала прочитайте раздел «0» и «Ключ шифрования» целиком — там главные подводные камни. Все действия сначала прогоняйте на копии, потом на бою.
0. Что переносим, а что настраиваем заново
| Данные | Действие |
|---|---|
Сотрудники (employees), кроме полей Mattermost/2FA |
переносим |
Аптеки/локации (locations), справочники (core Directory) |
переносим |
| Задачи, IT-заявки, коммуналка, найм, новости, графики | переносим |
Фото сотрудников/аптек (папка media/) |
переносим (rsync) |
Привязка к Mattermost (mattermost_* у Employee, таблицы mattermost_connector) |
сбрасываем — чат новый |
2FA (totp_secret, totp_enabled, totp_backup_codes) |
сбрасываем — перезаведут на проде |
Секреты коннекторов (ConnectorConfig.password/token) |
сбрасываем — заведём под прод-серверы |
Веб-сессии (django_session), лог админки |
не переносим |
1. Ключ шифрования FIELD_ENCRYPTION_KEY — главный нюанс
Этим ключом шифруются: ConnectorConfig.password/token, Employee.totp_secret,
MattermostUser.access_token_encrypted. Если на проде ключ другой, чем был на
тесте, эти поля не расшифруются.
Так как чат новый, коннекторы настраиваем заново, а 2FA сбрасываем — все зашифрованные поля мы всё равно обнуляем. Поэтому:
- на проде задайте свой новый
FIELD_ENCRYPTION_KEYв.env(не переиспользуйте тестовый); - после переноса выполните скрипт очистки (раздел 5) — он обнулит все шифрованные поля, которые «не читаются» новым ключом.
Сгенерировать ключ:
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Задать в .env прода до первого migrate/запуска:
FIELD_ENCRYPTION_KEY=<сгенерированный_ключ>
2. Подготовка боевого сервера
- Развернуть код нужного коммита (тот же, что на тесте — иначе разойдутся миграции):
git clone <repo> acore && cd acore && git checkout <нужный_коммит> python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt - Создать
.envпрода (по образцу.env.example):DJANGO_SETTINGS_MODULE=acore.settings.production DEBUG=False SECRET_KEY=<новый_секрет> ALLOWED_HOSTS=portal.ваш-домен DATABASE_URL=postgres://acore:пароль@localhost:5432/acore_prod FIELD_ENCRYPTION_KEY=<новый_ключ_из_шага_1> - Создать пустую боевую БД:
sudo -u postgres createuser acore -P sudo -u postgres createdb acore_prod -O acore - Статика и переводы:
python manage.py collectstatic --noinput python manage.py compilemessages -l uk -l en
Проверьте, что тест и прод на одном коммите:
git rev-parse HEADдолжен совпадать. Иначе схема БД будет разной.
3. Вариант A (рекомендуемый) — полный перенос БД через pg_dump
Самый надёжный способ, когда боевая БД пустая. Копирует схему + данные + счётчики ID. Шифрованные поля переносятся «как есть» и потом обнуляются скриптом.
На тесте — снять дамп:
pg_dump --no-owner --no-privileges "postgres://acore:пароль@ТЕСТ_ХОСТ:5432/acore_test" > acore.sql
Скопировать файл на прод (scp acore.sql prod:~/).
На проде — залить в пустую БД:
psql "postgres://acore:пароль@localhost:5432/acore_prod" < acore.sql
Догнать миграции (обычно no-op, если коммит тот же):
python manage.py migrate
Затем перейти к разделу 5 (очистка) и 6 (медиа).
4. Вариант B (альтернатива) — выборочный перенос через dumpdata/loaddata
Годится, если на проде уже есть данные, которые нельзя терять, или нужен
новый ключ шифрования без переноса секретов (dumpdata дампит расшифрованные
значения и заново шифрует их при загрузке прод-ключом).
На тесте — выгрузить только доменные приложения (без Mattermost, сессий, прав):
python manage.py dumpdata --natural-foreign --natural-primary --indent 2 \
-e contenttypes -e auth.permission -e admin.logentry -e sessions \
-e mattermost_connector \
employees locations core tasks it_manager utilities hiring feed visit_manager \
-o dump.json
На проде — сначала migrate (создаёт схему), затем загрузить:
python manage.py migrate
python manage.py loaddata dump.json
dump.jsonсодержитtotp_secretв открытом виде — храните файл аккуратно и удалите после загрузки. Послеloaddataвсё равно выполните раздел 5.
5. Очистка: сброс Mattermost, 2FA и секретов коннекторов
python manage.py shell и вставить:
from apps.employees.models import Employee
from core.models import ConnectorConfig
from django.contrib.sessions.models import Session
# 1) Отвязка от старого Mattermost (чат новый)
Employee.objects.update(
mattermost_user_id=None, mattermost_username=None,
mattermost_sync_status='pending', mattermost_last_sync=None,
)
# 2) Сброс 2FA — сотрудники заведут заново на проде
Employee.objects.update(totp_secret='', totp_enabled=False, totp_backup_codes=[])
# 3) Удаление синхронизированных объектов Mattermost
from apps.mattermost_connector.models import (
MattermostUser, MattermostTeam, MattermostChannel, MattermostBot,
)
MattermostUser.objects.all().delete()
MattermostChannel.objects.all().delete()
MattermostTeam.objects.all().delete()
MattermostBot.objects.all().delete()
# 4) Секреты коннекторов (зашифрованы старым ключом → обнуляем, заведём в админке)
for c in ConnectorConfig.objects.all():
c.is_enabled = False
c.password = ''
c.token = ''
c.save()
# 5) Сброс веб-сессий — все перелогинятся
Session.objects.all().delete()
print('Очистка выполнена')
Active Directory пока не подключён в реале — поля
ad_user_dn/ad_sync_statusу всех пустые,ad_sync_enabled=False. Отдельная AD-очистка при переносе не нужна. (Когда/если позже реально заведёте AD — настраивать его будете уже на проде с нуля, отдельной задачей.)
6. Перенос медиафайлов (фото)
Фото сотрудников и аптек лежат в media/. Скопировать с теста на прод:
rsync -avz ТЕСТ_ХОСТ:/путь/к/acore/media/ /путь/к/acore/media/
7. Настройка нового чата и коннекторов на проде
Реально в работе сейчас только Mattermost — его и настраиваем заново:
- Поднять/указать новый Mattermost; в админке портала
Core → Настройки коннекторовзавести коннекторmattermostзаново: host,public_url(в extra_config), токен бота. - Создать бота:
python manage.py create_mattermost_bot(или как заведено). - Синхронизация сотрудников в новый Mattermost — вашим обычным процессом (по одному через админку или пакетной командой синхронизации).
Коннекторы AD, Carbonio/почта, Nextcloud вживую ещё не подключены — при этом
переносе их трогать не нужно. Настроите их на проде отдельными задачами, когда
дойдёт до реального подключения; секреты тогда введёте заново (шифруются новым
FIELD_ENCRYPTION_KEY).
8. Первый вход и 2FA
2FA обязательна для staff и включается принудительно. После переноса:
- Зайдите суперпользователем (создайте, если нужно:
python manage.py createsuperuser). - При первом входе портал направит на
/2fa/setup/— отсканируйте QR, сохраните резервные коды. - Остальные staff-сотрудники заведут 2FA сами при первом входе.
9. Проверочный чек-лист
python manage.py check— без ошибок.- Сотрудники и аптеки на месте, количество совпадает с тестом.
- Фото сотрудников открываются (media перенесён).
- Ни у кого не осталось
mattermost_user_id(чат новый). totp_enabled=Falseу всех, вход требует заново настроить 2FA.- Коннекторы выключены/пустые, пока не заведёте боевые секреты.
- Суперпользователь заходит и проходит 2FA.
- Язык переключается (ru/uk/en), переводы на месте.
10. Частые грабли
- Разные коммиты test/prod → «нет таблицы/колонки» или конфликт миграций.
Держите оба на одном коммите; после переноса всегда
migrate. - Другой
FIELD_ENCRYPTION_KEYбез очистки шифрованных полей → ошибки расшифровки при чтении коннекторов/2FA. Всегда выполняйте раздел 5. - Забыли медиа → у сотрудников «нет фото». Не забудьте
rsync media/. - Счётчики ID (sequences) —
pg_dumpпереносит их сам; приloaddataPostgres обычно тоже корректен, но если увидите «duplicate key» при создании новых записей —python manage.py sqlsequencereset <app> | python manage.py dbshell. DEBUG=Falseбезcollectstatic→ нет статики/стилей. Соберите статику.- Не делайте перенос на живом проде без бэкапа: сначала
pg_dump acore_prod > backup_before.sql.