mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Auto migrating (#3741)
* base structure for updates * add base structure * add settingscheck * update docstring * only load plugins if needed * fix misstyping * run migration * check if there are open migrations * log open migration * add more logging * patch in fore reloading on unload * only run if database is ready * check every 5 minutes * remove non implemented feautres from desc * add command flag to makr if cmmand runs as worker * Add tests for migrations * factor mmigration plan into own function * Add print statements * add initial migrations for tests * remove last assertation * cleanup migrations after run * add flag to accept empty source code files * the flag is enough for reporting * fix test * do not run migrations on sqlite3 * make sure migrations don't fail if no plan ran * increase coverage for migration * spell fix * check for migrations daily * add a migration check after plugins are installed
This commit is contained in:
parent
75b223acc4
commit
5037e427b6
@ -12,10 +12,9 @@ from django.db import transaction
|
|||||||
from django.db.utils import IntegrityError
|
from django.db.utils import IntegrityError
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
from InvenTree.config import get_setting
|
||||||
from InvenTree.ready import canAppAccessDatabase, isInTestMode
|
from InvenTree.ready import canAppAccessDatabase, isInTestMode
|
||||||
|
|
||||||
from .config import get_setting
|
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
|
|
||||||
@ -24,8 +23,18 @@ class InvenTreeConfig(AppConfig):
|
|||||||
name = 'InvenTree'
|
name = 'InvenTree'
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
"""Setup background tasks and update exchange rates."""
|
"""Run system wide setup init steps.
|
||||||
|
|
||||||
|
Like:
|
||||||
|
- Checking if migrations should be run
|
||||||
|
- Cleaning up tasks
|
||||||
|
- Starting regular tasks
|
||||||
|
- Updateing exchange rates
|
||||||
|
- Collecting notification mehods
|
||||||
|
- Adding users set in the current environment
|
||||||
|
"""
|
||||||
if canAppAccessDatabase() or settings.TESTING_ENV:
|
if canAppAccessDatabase() or settings.TESTING_ENV:
|
||||||
|
InvenTree.tasks.check_for_migrations(worker=False)
|
||||||
|
|
||||||
self.remove_obsolete_tasks()
|
self.remove_obsolete_tasks()
|
||||||
|
|
||||||
|
12
InvenTree/InvenTree/migrations/0001_initial.py
Normal file
12
InvenTree/InvenTree/migrations/0001_initial.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Generated by Django 3.2.15 on 2022-10-03 18:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
]
|
0
InvenTree/InvenTree/migrations/__init__.py
Normal file
0
InvenTree/InvenTree/migrations/__init__.py
Normal file
@ -15,10 +15,17 @@ from django.conf import settings
|
|||||||
from django.core import mail as django_mail
|
from django.core import mail as django_mail
|
||||||
from django.core.exceptions import AppRegistryNotReady
|
from django.core.exceptions import AppRegistryNotReady
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.db.utils import OperationalError, ProgrammingError
|
from django.db import DEFAULT_DB_ALIAS, connections
|
||||||
|
from django.db.migrations.executor import MigrationExecutor
|
||||||
|
from django.db.utils import (NotSupportedError, OperationalError,
|
||||||
|
ProgrammingError)
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from maintenance_mode.core import (get_maintenance_mode, maintenance_mode_on,
|
||||||
|
set_maintenance_mode)
|
||||||
|
|
||||||
|
from InvenTree.config import get_setting
|
||||||
|
|
||||||
logger = logging.getLogger("inventree")
|
logger = logging.getLogger("inventree")
|
||||||
|
|
||||||
@ -506,3 +513,74 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
|
|||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
html_message=html_message
|
html_message=html_message
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
|
def check_for_migrations(worker: bool = True):
|
||||||
|
"""Checks if migrations are needed.
|
||||||
|
|
||||||
|
If the setting auto_update is enabled we will start updateing.
|
||||||
|
"""
|
||||||
|
# Test if auto-updates are enabled
|
||||||
|
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
|
||||||
|
return
|
||||||
|
|
||||||
|
from plugin import registry
|
||||||
|
|
||||||
|
plan = get_migration_plan()
|
||||||
|
|
||||||
|
# Check if there are any open migrations
|
||||||
|
if not plan:
|
||||||
|
logger.info('There are no open migrations')
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info('There are open migrations')
|
||||||
|
|
||||||
|
# Log open migrations
|
||||||
|
for migration in plan:
|
||||||
|
logger.info(migration[0])
|
||||||
|
|
||||||
|
# Set the application to maintenance mode - no access from now on.
|
||||||
|
logger.info('Going into maintenance')
|
||||||
|
set_maintenance_mode(True)
|
||||||
|
logger.info('Mainentance mode is on now')
|
||||||
|
|
||||||
|
# Check if we are worker - go kill all other workers then.
|
||||||
|
# Only the frontend workers run updates.
|
||||||
|
if worker:
|
||||||
|
logger.info('Current process is a worker - shutting down cluster')
|
||||||
|
|
||||||
|
# Ok now we are ready to go ahead!
|
||||||
|
# To be sure we are in maintenance this is wrapped
|
||||||
|
with maintenance_mode_on():
|
||||||
|
logger.info('Starting migrations')
|
||||||
|
print('Starting migrations')
|
||||||
|
|
||||||
|
try:
|
||||||
|
call_command('migrate', interactive=False)
|
||||||
|
except NotSupportedError as e: # pragma: no cover
|
||||||
|
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
|
||||||
|
raise e
|
||||||
|
logger.error(f'Error during migrations: {e}')
|
||||||
|
|
||||||
|
print('Migrations done')
|
||||||
|
logger.info('Ran migrations')
|
||||||
|
|
||||||
|
# Make sure we are out of maintenance again
|
||||||
|
logger.info('Checking InvenTree left maintenance mode')
|
||||||
|
if get_maintenance_mode():
|
||||||
|
|
||||||
|
logger.warning('Mainentance was still on - releasing now')
|
||||||
|
set_maintenance_mode(False)
|
||||||
|
logger.info('Released out of maintenance')
|
||||||
|
|
||||||
|
# We should be current now - triggering full reload to make sure all models
|
||||||
|
# are loaded fully in their new state.
|
||||||
|
registry.reload_plugins(full_reload=True, force_reload=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_migration_plan():
|
||||||
|
"""Returns a list of migrations which are needed to be run."""
|
||||||
|
executor = MigrationExecutor(connections[DEFAULT_DB_ALIAS])
|
||||||
|
plan = executor.migration_plan(executor.loader.graph.leaf_nodes())
|
||||||
|
return plan
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
"""Unit tests for task management."""
|
"""Unit tests for task management."""
|
||||||
|
|
||||||
|
import os
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django.db.utils import NotSupportedError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -117,3 +121,32 @@ class InvenTreeTaskTests(TestCase):
|
|||||||
response = InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION')
|
response = InvenTreeSetting.get_setting('_INVENTREE_LATEST_VERSION')
|
||||||
self.assertNotEqual(response, '')
|
self.assertNotEqual(response, '')
|
||||||
self.assertTrue(bool(response))
|
self.assertTrue(bool(response))
|
||||||
|
|
||||||
|
def test_task_check_for_migrations(self):
|
||||||
|
"""Test the task check_for_migrations."""
|
||||||
|
# Update disabled
|
||||||
|
InvenTree.tasks.check_for_migrations()
|
||||||
|
|
||||||
|
# Update enabled - no migrations
|
||||||
|
os.environ['INVENTREE_AUTO_UPDATE'] = 'True'
|
||||||
|
InvenTree.tasks.check_for_migrations()
|
||||||
|
|
||||||
|
# Create migration
|
||||||
|
self.assertEqual(len(InvenTree.tasks.get_migration_plan()), 0)
|
||||||
|
call_command('makemigrations', ['InvenTree', '--empty'], interactive=False)
|
||||||
|
self.assertEqual(len(InvenTree.tasks.get_migration_plan()), 1)
|
||||||
|
|
||||||
|
# Run with migrations - catch no foreigner error
|
||||||
|
try:
|
||||||
|
InvenTree.tasks.check_for_migrations()
|
||||||
|
except NotSupportedError as e: # pragma: no cover
|
||||||
|
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
try:
|
||||||
|
migration_name = InvenTree.tasks.get_migration_plan()[0][0].name + '.py'
|
||||||
|
migration_path = settings.BASE_DIR / 'InvenTree' / 'migrations' / migration_name
|
||||||
|
migration_path.unlink()
|
||||||
|
except IndexError: # pragma: no cover
|
||||||
|
pass
|
||||||
|
@ -119,6 +119,10 @@ plugins_enabled: False
|
|||||||
#plugin_file: '/path/to/plugins.txt'
|
#plugin_file: '/path/to/plugins.txt'
|
||||||
#plugin_dir: '/path/to/plugins/'
|
#plugin_dir: '/path/to/plugins/'
|
||||||
|
|
||||||
|
# Set this variable to True to enable auto-migrations
|
||||||
|
# Alternatively, use the environment variable INVENTREE_AUTO_UPDATE
|
||||||
|
auto_update: False
|
||||||
|
|
||||||
# Allowed hosts (see ALLOWED_HOSTS in Django settings documentation)
|
# Allowed hosts (see ALLOWED_HOSTS in Django settings documentation)
|
||||||
# A list of strings representing the host/domain names that this Django site can serve.
|
# A list of strings representing the host/domain names that this Django site can serve.
|
||||||
# Default behaviour is to allow all hosts (THIS IS NOT SECURE!)
|
# Default behaviour is to allow all hosts (THIS IS NOT SECURE!)
|
||||||
|
@ -162,8 +162,12 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
logger.info('Finished loading plugins')
|
logger.info('Finished loading plugins')
|
||||||
|
|
||||||
def unload_plugins(self):
|
def unload_plugins(self, force_reload: bool = False):
|
||||||
"""Unload and deactivate all IntegrationPlugins."""
|
"""Unload and deactivate all IntegrationPlugins.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
|
"""
|
||||||
|
|
||||||
logger.info('Start unloading plugins')
|
logger.info('Start unloading plugins')
|
||||||
|
|
||||||
@ -176,7 +180,7 @@ class PluginsRegistry:
|
|||||||
self._clean_registry()
|
self._clean_registry()
|
||||||
|
|
||||||
# deactivate all integrations
|
# deactivate all integrations
|
||||||
self._deactivate_plugins()
|
self._deactivate_plugins(force_reload=force_reload)
|
||||||
|
|
||||||
# remove maintenance
|
# remove maintenance
|
||||||
if not _maintenance:
|
if not _maintenance:
|
||||||
@ -184,11 +188,12 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
logger.info('Finished unloading plugins')
|
logger.info('Finished unloading plugins')
|
||||||
|
|
||||||
def reload_plugins(self, full_reload: bool = False):
|
def reload_plugins(self, full_reload: bool = False, force_reload: bool = False):
|
||||||
"""Safely reload.
|
"""Safely reload.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
full_reload (bool, optional): Reload everything - including plugin mechanism. Defaults to False.
|
||||||
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
"""
|
"""
|
||||||
# Do not reload whe currently loading
|
# Do not reload whe currently loading
|
||||||
if self.is_loading:
|
if self.is_loading:
|
||||||
@ -197,8 +202,8 @@ class PluginsRegistry:
|
|||||||
logger.info('Start reloading plugins')
|
logger.info('Start reloading plugins')
|
||||||
|
|
||||||
with maintenance_mode_on():
|
with maintenance_mode_on():
|
||||||
self.unload_plugins()
|
self.unload_plugins(force_reload=force_reload)
|
||||||
self.load_plugins(full_reload)
|
self.load_plugins(full_reload=full_reload)
|
||||||
|
|
||||||
logger.info('Finished reloading plugins')
|
logger.info('Finished reloading plugins')
|
||||||
|
|
||||||
@ -470,9 +475,13 @@ class PluginsRegistry:
|
|||||||
self.activate_plugin_app(plugins, force_reload=force_reload, full_reload=full_reload)
|
self.activate_plugin_app(plugins, force_reload=force_reload, full_reload=full_reload)
|
||||||
self.activate_plugin_url(plugins, force_reload=force_reload, full_reload=full_reload)
|
self.activate_plugin_url(plugins, force_reload=force_reload, full_reload=full_reload)
|
||||||
|
|
||||||
def _deactivate_plugins(self):
|
def _deactivate_plugins(self, force_reload: bool = False):
|
||||||
"""Run deactivation functions for all plugins."""
|
"""Run deactivation functions for all plugins.
|
||||||
self.deactivate_plugin_app()
|
|
||||||
|
Args:
|
||||||
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
|
"""
|
||||||
|
self.deactivate_plugin_app(force_reload=force_reload)
|
||||||
self.deactivate_plugin_schedule()
|
self.deactivate_plugin_schedule()
|
||||||
self.deactivate_plugin_settings()
|
self.deactivate_plugin_settings()
|
||||||
# endregion
|
# endregion
|
||||||
@ -655,8 +664,12 @@ class PluginsRegistry:
|
|||||||
plugin_path = plugin.__module__.split('.')[0]
|
plugin_path = plugin.__module__.split('.')[0]
|
||||||
return plugin_path
|
return plugin_path
|
||||||
|
|
||||||
def deactivate_plugin_app(self):
|
def deactivate_plugin_app(self, force_reload: bool = False):
|
||||||
"""Deactivate AppMixin plugins - some magic required."""
|
"""Deactivate AppMixin plugins - some magic required.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
force_reload (bool, optional): Also reload base apps. Defaults to False.
|
||||||
|
"""
|
||||||
# unregister models from admin
|
# unregister models from admin
|
||||||
for plugin_path in self.installed_apps:
|
for plugin_path in self.installed_apps:
|
||||||
models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed
|
models = [] # the modelrefs need to be collected as poping an item in a iter is not welcomed
|
||||||
@ -693,7 +706,7 @@ class PluginsRegistry:
|
|||||||
|
|
||||||
# reset load flag and reload apps
|
# reset load flag and reload apps
|
||||||
settings.INTEGRATION_APPS_LOADED = False
|
settings.INTEGRATION_APPS_LOADED = False
|
||||||
self._reload_apps()
|
self._reload_apps(force_reload=force_reload)
|
||||||
|
|
||||||
# update urls to remove the apps from the site admin
|
# update urls to remove the apps from the site admin
|
||||||
self._update_urls()
|
self._update_urls()
|
||||||
|
@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from common.serializers import GenericReferencedSettingSerializer
|
from common.serializers import GenericReferencedSettingSerializer
|
||||||
|
from InvenTree.tasks import check_for_migrations, offload_task
|
||||||
from plugin.models import NotificationUserSetting, PluginConfig, PluginSetting
|
from plugin.models import NotificationUserSetting, PluginConfig, PluginSetting
|
||||||
|
|
||||||
|
|
||||||
@ -156,6 +157,9 @@ class PluginConfigInstallSerializer(serializers.Serializer):
|
|||||||
with open(settings.PLUGIN_FILE, "a") as plugin_file:
|
with open(settings.PLUGIN_FILE, "a") as plugin_file:
|
||||||
plugin_file.write(f'{" ".join(install_name)} # Installed {timezone.now()} by {str(self.context["request"].user)}\n')
|
plugin_file.write(f'{" ".join(install_name)} # Installed {timezone.now()} by {str(self.context["request"].user)}\n')
|
||||||
|
|
||||||
|
# Check for migrations
|
||||||
|
offload_task(check_for_migrations, worker=True)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user