Files
ACORE/docs/перенос-на-прод.md
T

13 KiB
Raw Blame History

Перенос данных на боевой сервер (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. Подготовка боевого сервера

  1. Развернуть код нужного коммита (тот же, что на тесте — иначе разойдутся миграции):
    git clone <repo> 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. Создать пустую боевую БД:
    sudo -u postgres createuser acore -P
    sudo -u postgres createdb acore_prod -O acore
    
  4. Статика и переводы:
    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 — его и настраиваем заново:

  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 <app> | python manage.py dbshell.
  • DEBUG=False без collectstatic → нет статики/стилей. Соберите статику.
  • Не делайте перенос на живом проде без бэкапа: сначала pg_dump acore_prod > backup_before.sql.