mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Plugin auto migrate (#5668)
* Improved handling of race condition when saving setting value * Improvements for managing pending migrations - Inform user if there are outstanding migrations - reload plugin registry (if necessary) * Increase django-q polling time According to this thread, should reduce multiple workers taking the same task: https://github.com/Koed00/django-q/issues/183#issuecomment-239676084 * Revert default behavior * Better logging * Remove comment * Update unit test * Revert maintenance mode behaviour * raise ValidationError in settings
This commit is contained in:
parent
c7eb90347a
commit
7ab5ddcd7d
@ -40,7 +40,6 @@ class InvenTreeConfig(AppConfig):
|
|||||||
return
|
return
|
||||||
|
|
||||||
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()
|
||||||
|
|
||||||
@ -49,6 +48,8 @@ class InvenTreeConfig(AppConfig):
|
|||||||
|
|
||||||
if not isInTestMode(): # pragma: no cover
|
if not isInTestMode(): # pragma: no cover
|
||||||
self.update_exchange_rates()
|
self.update_exchange_rates()
|
||||||
|
# Let the background worker check for migrations
|
||||||
|
InvenTree.tasks.offload_task(InvenTree.tasks.check_for_migrations)
|
||||||
|
|
||||||
self.collect_notification_methods()
|
self.collect_notification_methods()
|
||||||
|
|
||||||
|
@ -751,6 +751,7 @@ Q_CLUSTER = {
|
|||||||
'orm': 'default',
|
'orm': 'default',
|
||||||
'cache': 'default',
|
'cache': 'default',
|
||||||
'sync': False,
|
'sync': False,
|
||||||
|
'poll': 1.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configure django-q sentry integration
|
# Configure django-q sentry integration
|
||||||
|
@ -569,46 +569,60 @@ def run_backup():
|
|||||||
record_task_success('run_backup')
|
record_task_success('run_backup')
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
@scheduled_task(ScheduledTask.DAILY)
|
@scheduled_task(ScheduledTask.DAILY)
|
||||||
def check_for_migrations(worker: bool = True):
|
def check_for_migrations():
|
||||||
"""Checks if migrations are needed.
|
"""Checks if migrations are needed.
|
||||||
|
|
||||||
If the setting auto_update is enabled we will start updating.
|
If the setting auto_update is enabled we will start updating.
|
||||||
"""
|
"""
|
||||||
# Test if auto-updates are enabled
|
|
||||||
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
|
|
||||||
return
|
|
||||||
|
|
||||||
|
from common.models import InvenTreeSetting
|
||||||
from plugin import registry
|
from plugin import registry
|
||||||
|
|
||||||
|
def set_pending_migrations(n: int):
|
||||||
|
"""Helper function to inform the user about pending migrations"""
|
||||||
|
|
||||||
|
logger.info('There are %s pending migrations', n)
|
||||||
|
InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None)
|
||||||
|
|
||||||
|
logger.info("Checking for pending database migrations")
|
||||||
|
|
||||||
|
# Force plugin registry reload
|
||||||
|
registry.check_reload()
|
||||||
|
|
||||||
plan = get_migration_plan()
|
plan = get_migration_plan()
|
||||||
|
|
||||||
|
n = len(plan)
|
||||||
|
|
||||||
# Check if there are any open migrations
|
# Check if there are any open migrations
|
||||||
if not plan:
|
if not plan:
|
||||||
logger.info('There are no open migrations')
|
set_pending_migrations(0)
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.info('There are open migrations')
|
set_pending_migrations(n)
|
||||||
|
|
||||||
|
# Test if auto-updates are enabled
|
||||||
|
if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
|
||||||
|
logger.info("Auto-update is disabled - skipping migrations")
|
||||||
|
return
|
||||||
|
|
||||||
# Log open migrations
|
# Log open migrations
|
||||||
for migration in plan:
|
for migration in plan:
|
||||||
logger.info(migration[0])
|
logger.info("- %s", str(migration[0]))
|
||||||
|
|
||||||
# Set the application to maintenance mode - no access from now on.
|
# Set the application to maintenance mode - no access from now on.
|
||||||
logger.info('Going into maintenance')
|
|
||||||
set_maintenance_mode(True)
|
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
|
# To be sure we are in maintenance this is wrapped
|
||||||
with maintenance_mode_on():
|
with maintenance_mode_on():
|
||||||
logger.info('Starting migrations')
|
logger.info('Starting migration process...')
|
||||||
print('Starting migrations')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
call_command('migrate', interactive=False)
|
call_command('migrate', interactive=False)
|
||||||
@ -616,25 +630,17 @@ def check_for_migrations(worker: bool = True):
|
|||||||
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
|
if settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3':
|
||||||
raise e
|
raise e
|
||||||
logger.exception('Error during migrations: %s', e)
|
logger.exception('Error during migrations: %s', e)
|
||||||
|
else:
|
||||||
|
set_pending_migrations(0)
|
||||||
|
|
||||||
print('Migrations done')
|
logger.info("Completed %s migrations", n)
|
||||||
logger.info('Ran migrations')
|
|
||||||
|
|
||||||
# Make sure we are out of maintenance again
|
# Make sure we are out of maintenance mode
|
||||||
logger.info('Checking InvenTree left maintenance mode')
|
|
||||||
if get_maintenance_mode():
|
if get_maintenance_mode():
|
||||||
|
logger.warning("Maintenance mode was not disabled - forcing it now")
|
||||||
logger.warning('Mainentance was still on - releasing now')
|
|
||||||
set_maintenance_mode(False)
|
set_maintenance_mode(False)
|
||||||
logger.info('Released out of maintenance')
|
logger.info("Manually released maintenance mode")
|
||||||
|
|
||||||
# We should be current now - triggering full reload to make sure all models
|
# We should be current now - triggering full reload to make sure all models
|
||||||
# are loaded fully in their new state.
|
# are loaded fully in their new state.
|
||||||
registry.reload_plugins(full_reload=True, force_reload=True)
|
registry.reload_plugins(full_reload=True, force_reload=True, collect=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
|
|
||||||
|
@ -595,6 +595,8 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
if change_user is not None and not change_user.is_staff:
|
if change_user is not None and not change_user.is_staff:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
attempts = int(kwargs.get('attempts', 3))
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
'key__iexact': key,
|
'key__iexact': key,
|
||||||
|
|
||||||
@ -615,8 +617,22 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
if setting.is_bool():
|
if setting.is_bool():
|
||||||
value = InvenTree.helpers.str2bool(value)
|
value = InvenTree.helpers.str2bool(value)
|
||||||
|
|
||||||
setting.value = str(value)
|
try:
|
||||||
setting.save()
|
setting.value = str(value)
|
||||||
|
setting.save()
|
||||||
|
except ValidationError as exc:
|
||||||
|
# We need to know about validation errors
|
||||||
|
raise exc
|
||||||
|
except IntegrityError:
|
||||||
|
# Likely a race condition has caused a duplicate entry to be created
|
||||||
|
if attempts > 0:
|
||||||
|
# Try again
|
||||||
|
logger.info("Duplicate setting key '%s' for %s - trying again", key, str(cls))
|
||||||
|
cls.set_setting(key, value, change_user, create=create, attempts=attempts - 1, **kwargs)
|
||||||
|
except Exception as exc:
|
||||||
|
# Some other error
|
||||||
|
logger.exception("Error setting setting '%s' for %s: %s", key, str(cls), str(type(exc)))
|
||||||
|
pass
|
||||||
|
|
||||||
key = models.CharField(max_length=50, blank=False, unique=False, help_text=_('Settings key (must be unique - case insensitive)'))
|
key = models.CharField(max_length=50, blank=False, unique=False, help_text=_('Settings key (must be unique - case insensitive)'))
|
||||||
|
|
||||||
@ -1050,6 +1066,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'hidden': True,
|
'hidden': True,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'_PENDING_MIGRATIONS': {
|
||||||
|
'name': _('Pending migrations'),
|
||||||
|
'description': _('Number of pending database migrations'),
|
||||||
|
'default': 0,
|
||||||
|
'validator': int,
|
||||||
|
},
|
||||||
|
|
||||||
'INVENTREE_INSTANCE': {
|
'INVENTREE_INSTANCE': {
|
||||||
'name': _('Server Instance Name'),
|
'name': _('Server Instance Name'),
|
||||||
'default': 'InvenTree',
|
'default': 'InvenTree',
|
||||||
|
@ -335,8 +335,10 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
response = self.get(url, expected_code=200)
|
response = self.get(url, expected_code=200)
|
||||||
|
|
||||||
|
n_public_settings = len([k for k in InvenTreeSetting.SETTINGS.keys() if not k.startswith('_')])
|
||||||
|
|
||||||
# Number of results should match the number of settings
|
# Number of results should match the number of settings
|
||||||
self.assertEqual(len(response.data), len(InvenTreeSetting.SETTINGS.keys()))
|
self.assertEqual(len(response.data), n_public_settings)
|
||||||
|
|
||||||
def test_company_name(self):
|
def test_company_name(self):
|
||||||
"""Test a settings object lifecycle e2e."""
|
"""Test a settings object lifecycle e2e."""
|
||||||
|
@ -146,8 +146,15 @@ class PluginActivateSerializer(serializers.Serializer):
|
|||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
"""Apply the new 'active' value to the plugin instance"""
|
"""Apply the new 'active' value to the plugin instance"""
|
||||||
|
|
||||||
|
from InvenTree.tasks import check_for_migrations, offload_task
|
||||||
|
|
||||||
instance.active = validated_data.get('active', True)
|
instance.active = validated_data.get('active', True)
|
||||||
instance.save()
|
instance.save()
|
||||||
|
|
||||||
|
if instance.active:
|
||||||
|
# A plugin has just been activated - check for database migrations
|
||||||
|
offload_task(check_for_migrations)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
{% settings_value 'RETURNORDER_ENABLED' as return_order_enabled %}
|
{% settings_value 'RETURNORDER_ENABLED' as return_order_enabled %}
|
||||||
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
{% settings_value "REPORT_ENABLE" as report_enabled %}
|
||||||
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
|
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
|
||||||
|
{% settings_value "_PENDING_MIGRATIONS" as pending_migrations %}
|
||||||
{% settings_value "LABEL_ENABLE" as labels_enabled %}
|
{% settings_value "LABEL_ENABLE" as labels_enabled %}
|
||||||
{% inventree_show_about user as show_about %}
|
{% inventree_show_about user as show_about %}
|
||||||
|
|
||||||
@ -106,6 +107,16 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if pending_migrations > 0 %}
|
||||||
|
<div id='alert-pending-migrations' class='alert alert-danger' role='alert'>
|
||||||
|
<span class='fas fa-database'></span>
|
||||||
|
<strong>{% trans "Pending Database Migrations" %}</strong>
|
||||||
|
<small>
|
||||||
|
<br>
|
||||||
|
{% trans "There are pending database migrations which require attention" %}. {% trans "Contact your system administrator for further information" %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock alerts %}
|
{% endblock alerts %}
|
||||||
|
|
||||||
|
@ -17,3 +17,6 @@ INVENTREE_DB_PASSWORD=pgpassword
|
|||||||
|
|
||||||
# Enable custom plugins?
|
# Enable custom plugins?
|
||||||
INVENTREE_PLUGINS_ENABLED=True
|
INVENTREE_PLUGINS_ENABLED=True
|
||||||
|
|
||||||
|
# Auto run migrations?
|
||||||
|
INVENTREE_AUTO_UPDATE=False
|
||||||
|
@ -47,6 +47,9 @@ INVENTREE_GUNICORN_TIMEOUT=90
|
|||||||
# Enable custom plugins?
|
# Enable custom plugins?
|
||||||
INVENTREE_PLUGINS_ENABLED=False
|
INVENTREE_PLUGINS_ENABLED=False
|
||||||
|
|
||||||
|
# Run migrations automatically?
|
||||||
|
INVENTREE_AUTO_UPDATE=False
|
||||||
|
|
||||||
# Image tag that should be used
|
# Image tag that should be used
|
||||||
INVENTREE_TAG=stable
|
INVENTREE_TAG=stable
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user