# Перенос данных на боевой сервер (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) — он обнулит все шифрованные поля, которые «не читаются» новым ключом. Сгенерировать ключ: ```bash python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ``` Задать в `.env` прода **до первого `migrate`/запуска**: ``` FIELD_ENCRYPTION_KEY=<сгенерированный_ключ> ``` --- ## 2. Подготовка боевого сервера 1. Развернуть код нужного коммита (тот же, что на тесте — иначе разойдутся миграции): ```bash git clone acore && cd acore && git checkout <нужный_коммит> python -m venv .venv && source .venv/bin/activate pip install -r requirements.txt ``` 2. Создать `.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> ``` 3. Создать пустую боевую БД: ```bash sudo -u postgres createuser acore -P sudo -u postgres createdb acore_prod -O acore ``` 4. Статика и переводы: ```bash python manage.py collectstatic --noinput python manage.py compilemessages -l uk -l en ``` > Проверьте, что тест и прод на одном коммите: > `git rev-parse HEAD` должен совпадать. Иначе схема БД будет разной. --- ## 3. Вариант A (рекомендуемый) — полный перенос БД через `pg_dump` Самый надёжный способ, когда **боевая БД пустая**. Копирует схему + данные + счётчики ID. Шифрованные поля переносятся «как есть» и потом обнуляются скриптом. **На тесте** — снять дамп: ```bash pg_dump --no-owner --no-privileges "postgres://acore:пароль@ТЕСТ_ХОСТ:5432/acore_test" > acore.sql ``` Скопировать файл на прод (`scp acore.sql prod:~/`). **На проде** — залить в пустую БД: ```bash psql "postgres://acore:пароль@localhost:5432/acore_prod" < acore.sql ``` Догнать миграции (обычно no-op, если коммит тот же): ```bash python manage.py migrate ``` Затем перейти к разделу **5 (очистка)** и **6 (медиа)**. --- ## 4. Вариант B (альтернатива) — выборочный перенос через `dumpdata/loaddata` Годится, если **на проде уже есть данные**, которые нельзя терять, или нужен новый ключ шифрования без переноса секретов (`dumpdata` дампит расшифрованные значения и заново шифрует их при загрузке прод-ключом). **На тесте** — выгрузить только доменные приложения (без Mattermost, сессий, прав): ```bash 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` (создаёт схему), затем загрузить: ```bash python manage.py migrate python manage.py loaddata dump.json ``` > `dump.json` содержит `totp_secret` в открытом виде — храните файл аккуратно и > удалите после загрузки. После `loaddata` всё равно выполните раздел 5. --- ## 5. Очистка: сброс Mattermost, 2FA и секретов коннекторов `python manage.py shell` и вставить: ```python 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/`. Скопировать с теста на прод: ```bash rsync -avz ТЕСТ_ХОСТ:/путь/к/acore/media/ /путь/к/acore/media/ ``` --- ## 7. Настройка нового чата и коннекторов на проде Реально в работе сейчас только **Mattermost** — его и настраиваем заново: 1. Поднять/указать **новый Mattermost**; в админке портала `Core → Настройки коннекторов` завести коннектор `mattermost` заново: host, `public_url` (в extra_config), токен бота. 2. Создать бота: `python manage.py create_mattermost_bot` (или как заведено). 3. Синхронизация сотрудников в новый Mattermost — вашим обычным процессом (по одному через админку или пакетной командой синхронизации). Коннекторы **AD, Carbonio/почта, Nextcloud вживую ещё не подключены** — при этом переносе их трогать не нужно. Настроите их на проде отдельными задачами, когда дойдёт до реального подключения; секреты тогда введёте заново (шифруются новым `FIELD_ENCRYPTION_KEY`). --- ## 8. Первый вход и 2FA 2FA обязательна для staff и включается принудительно. После переноса: 1. Зайдите суперпользователем (создайте, если нужно: `python manage.py createsuperuser`). 2. При первом входе портал направит на `/2fa/setup/` — отсканируйте QR, сохраните резервные коды. 3. Остальные 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` переносит их сам; при `loaddata` Postgres обычно тоже корректен, но если увидите «duplicate key» при создании новых записей — `python manage.py sqlsequencereset | python manage.py dbshell`. - **`DEBUG=False` без `collectstatic`** → нет статики/стилей. Соберите статику. - **Не делайте перенос на живом проде без бэкапа**: сначала `pg_dump acore_prod > backup_before.sql`.