Skip to content

Commit 937fa55

Browse files
committed
👌 Allow config downgrade for known migrations (#7491)
Allow `verdi config downgrade` to run when the current AiiDA version cannot load the configuration for normal use, but still has the migration path needed to downgrade it. Introduce a maximum downgradeable configuration version that is separate from the normally supported version, and use it to detect when the CLI can safely skip loading the default profile. This change allows to downgrade the config without requiring the current config schema version be the latest. It only l requires that the migration from newer to older version is implemented. This allows to include migrations for older releases through patch releases. (cherry picked from commit 7f36513)
1 parent f83e236 commit 937fa55

5 files changed

Lines changed: 128 additions & 15 deletions

File tree

‎src/aiida/cmdline/utils/defaults.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def get_default_profile():
2525
"""
2626
try:
2727
config = get_config(create=True)
28+
except exceptions.ConfigurationVersionError as exception:
29+
if exception._can_downgrade:
30+
return None
31+
echo.echo_critical(str(exception))
2832
except exceptions.ConfigurationError as exception:
2933
echo.echo_critical(str(exception))
3034

‎src/aiida/common/exceptions.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,10 @@ class MissingConfigurationError(ConfigurationError):
180180

181181

182182
class ConfigurationVersionError(ConfigurationError):
183-
"""Configuration error raised when the configuration file version is not
184-
compatible with the current version.
185-
"""
183+
"""Configuration error raised when the configuration file version is not compatible with the current version."""
184+
185+
# Hotfix see issue #7493
186+
_can_downgrade: bool = False
186187

187188

188189
class ClosedStorage(AiidaException):

‎src/aiida/manage/configuration/migrations/migrations.py‎

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434

3535
CURRENT_CONFIG_VERSION = 9
3636
OLDEST_COMPATIBLE_CONFIG_VERSION = 9
37+
# Highest configuration version for which this code can run downgrade migrations, even if it cannot load it.
38+
MAXIMUM_DOWNGRADE_CONFIG_VERSION = 10
3739

3840
CONFIG_LOGGER = AIIDA_LOGGER.getChild('config')
3941

@@ -497,6 +499,41 @@ def get_oldest_compatible_version(config):
497499
return config.get('CONFIG_VERSION', {}).get('OLDEST_COMPATIBLE', 0)
498500

499501

502+
def config_can_be_downgraded(
503+
config: ConfigType,
504+
target: Optional[int] = None,
505+
migrations: Iterable[type[SingleMigration]] = MIGRATIONS,
506+
) -> bool:
507+
"""Return whether the configuration can be downgraded to the target version.
508+
509+
This is intentionally distinct from :data:`CURRENT_CONFIG_VERSION`: an AiiDA version may not be able to load a
510+
configuration for normal operation, but may still know enough migrations to rewrite it for an older version.
511+
512+
:param config: the configuration dictionary
513+
:param target: the version to downgrade to, defaulting to :data:`CURRENT_CONFIG_VERSION`
514+
:param migrations: the registered migrations to consider
515+
:return: ``True`` if a chain of migrations exists to downgrade the configuration to the target version
516+
"""
517+
target = CURRENT_CONFIG_VERSION if target is None else target
518+
current = get_current_version(config)
519+
520+
if current <= target or current > MAXIMUM_DOWNGRADE_CONFIG_VERSION:
521+
return False
522+
523+
used = []
524+
while current > target:
525+
try:
526+
migrator = next(m for m in migrations if m.up_revision == current)
527+
except StopIteration:
528+
return False
529+
if migrator in used:
530+
return False
531+
used.append(migrator)
532+
current = migrator.down_revision
533+
534+
return current == target
535+
536+
500537
def upgrade_config(
501538
config: ConfigType, target: int = CURRENT_CONFIG_VERSION, migrations: Iterable[Type[SingleMigration]] = MIGRATIONS
502539
) -> ConfigType:
@@ -534,6 +571,13 @@ def downgrade_config(
534571
:return: the migrated configuration dictionary
535572
"""
536573
current = get_current_version(config)
574+
if current > MAXIMUM_DOWNGRADE_CONFIG_VERSION:
575+
msg = (
576+
f'Cannot downgrade configuration version {current}: this AiiDA version can only downgrade configuration '
577+
f'versions up to {MAXIMUM_DOWNGRADE_CONFIG_VERSION}.'
578+
)
579+
raise exceptions.ConfigurationError(msg)
580+
537581
used = []
538582
while current > target:
539583
current = get_current_version(config)
@@ -580,13 +624,25 @@ def config_needs_migrating(config, filepath: Optional[str] = None):
580624

581625
if oldest_compatible_version > CURRENT_CONFIG_VERSION:
582626
filepath = filepath if filepath else ''
583-
msg = (
584-
f'The AiiDA configuration file {filepath} has version {current_version} which is not compatible with '
585-
f'the current aiida version supporting up to version {CURRENT_CONFIG_VERSION}. '
586-
'Before switching to an older AiiDA version, use a newer AiiDA version that supports '
587-
f' at least version {CURRENT_CONFIG_VERSION} and run `verdi config downgrade {CURRENT_CONFIG_VERSION}` '
588-
f' to rewrite it for an older version. See {URL_CONFIG_SCHEMA_COMPATIBILITY} for the compatibility table.'
589-
)
590-
raise exceptions.ConfigurationVersionError(msg)
627+
can_downgrade = config_can_be_downgraded(config)
628+
if can_downgrade:
629+
msg = (
630+
f'The AiiDA configuration file {filepath} has version {current_version} which is not compatible with '
631+
f'the current aiida version supporting up to version {CURRENT_CONFIG_VERSION}. '
632+
f'Run `verdi config downgrade {CURRENT_CONFIG_VERSION}` to rewrite it for this version. See '
633+
f'{URL_CONFIG_SCHEMA_COMPATIBILITY} for the compatibility table.'
634+
)
635+
else:
636+
msg = (
637+
f'The AiiDA configuration file {filepath} has version {current_version} which is not compatible with '
638+
f'the current aiida version supporting up to version {CURRENT_CONFIG_VERSION}. '
639+
'Before switching to an older AiiDA version, use a newer AiiDA version that supports '
640+
f'configuration version {current_version} and run `verdi config downgrade {CURRENT_CONFIG_VERSION}` '
641+
'to rewrite it for this version. See '
642+
f'{URL_CONFIG_SCHEMA_COMPATIBILITY} for the compatibility table.'
643+
)
644+
error = exceptions.ConfigurationVersionError(msg)
645+
error._can_downgrade = can_downgrade
646+
raise error
591647

592648
return CURRENT_CONFIG_VERSION > current_version

‎tests/cmdline/commands/test_config.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
###########################################################################
99
"""Tests for ``verdi config``."""
1010

11+
import json
12+
import pathlib
13+
1114
import pytest
1215

1316
from aiida import get_profile
@@ -209,3 +212,30 @@ def test_config_downgrade(run_cli_command, config_with_profile_factory):
209212
options = ['config', 'downgrade', '1']
210213
result = run_cli_command(cmd_verdi.verdi, options, use_subprocess=False)
211214
assert 'Success: Downgraded' in result.output.strip()
215+
216+
217+
def test_config_downgrade_incompatible_but_downgradeable(run_cli_command, config_with_profile_factory, monkeypatch):
218+
"""Test `verdi config downgrade` can run even if the config cannot be loaded normally."""
219+
from aiida.manage import configuration
220+
from aiida.manage.configuration.migrations import migrations
221+
222+
config = config_with_profile_factory()
223+
filepath = pathlib.Path(config.filepath)
224+
dictionary = config.dictionary
225+
dictionary['CONFIG_VERSION'] = {'CURRENT': 10, 'OLDEST_COMPATIBLE': 10}
226+
dictionary.setdefault('options', {})['broker.task_timeout'] = 12
227+
filepath.write_text(json.dumps(dictionary), encoding='utf8')
228+
229+
configuration.CONFIG = None
230+
monkeypatch.setattr(migrations, 'CURRENT_CONFIG_VERSION', 9)
231+
monkeypatch.setattr(migrations, 'MAXIMUM_DOWNGRADE_CONFIG_VERSION', 10)
232+
233+
result = run_cli_command(
234+
cmd_verdi.verdi, ['config', 'downgrade', '9'], initialize_ctx_obj=False, use_subprocess=False
235+
)
236+
237+
assert 'Success: Downgraded' in result.output.strip()
238+
dictionary = json.loads(filepath.read_text(encoding='utf8'))
239+
assert dictionary['CONFIG_VERSION'] == {'CURRENT': 9, 'OLDEST_COMPATIBLE': 9}
240+
assert dictionary['options']['rmq.task_timeout'] == 12
241+
assert 'broker.task_timeout' not in dictionary['options']

‎tests/manage/configuration/migrations/test_migrations.py‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@
1818
from aiida.common.exceptions import ConfigurationError, ConfigurationVersionError
1919
from aiida.manage.configuration.migrations import check_and_migrate_config
2020
from aiida.manage.configuration.migrations.migrations import (
21-
CURRENT_CONFIG_VERSION,
21+
MAXIMUM_DOWNGRADE_CONFIG_VERSION,
2222
MIGRATIONS,
2323
Initial,
24+
config_can_be_downgraded,
2425
downgrade_config,
2526
upgrade_config,
2627
)
@@ -70,15 +71,36 @@ def test_downgrade_path_fail(load_config_sample):
7071
downgrade_config(config_initial, 4, migrations=[CircularMigration])
7172

7273

73-
def test_config_needs_migrating_incompatible_version():
74+
def test_config_needs_migrating_incompatible_version(monkeypatch):
7475
"""An incompatible configuration version should raise with downgrade guidance."""
76+
from aiida.manage.configuration.migrations import migrations
77+
78+
# Lower the loadable version below the downgrade cap so a config at the cap is unloadable but downgradeable,
79+
# exercising the can_downgrade guidance path (config_needs_migrating downgrades to CURRENT_CONFIG_VERSION).
80+
monkeypatch.setattr(migrations, 'CURRENT_CONFIG_VERSION', MAXIMUM_DOWNGRADE_CONFIG_VERSION - 1)
7581
config = {
76-
'CONFIG_VERSION': {'CURRENT': CURRENT_CONFIG_VERSION + 1, 'OLDEST_COMPATIBLE': CURRENT_CONFIG_VERSION + 1}
82+
'CONFIG_VERSION': {
83+
'CURRENT': MAXIMUM_DOWNGRADE_CONFIG_VERSION,
84+
'OLDEST_COMPATIBLE': MAXIMUM_DOWNGRADE_CONFIG_VERSION,
85+
}
7786
}
7887

79-
with pytest.raises(ConfigurationVersionError, match=r'verdi config downgrade'):
88+
with pytest.raises(ConfigurationVersionError, match=r'verdi config downgrade') as exception:
8089
check_and_migrate_config(config, filepath='/tmp/config.json')
8190

91+
assert exception.value._can_downgrade
92+
93+
94+
def test_config_can_be_downgraded():
95+
"""Test detecting whether a configuration can be downgraded by this AiiDA version."""
96+
assert config_can_be_downgraded(
97+
{'CONFIG_VERSION': {'CURRENT': MAXIMUM_DOWNGRADE_CONFIG_VERSION, 'OLDEST_COMPATIBLE': 0}},
98+
target=MAXIMUM_DOWNGRADE_CONFIG_VERSION - 1,
99+
)
100+
assert not config_can_be_downgraded(
101+
{'CONFIG_VERSION': {'CURRENT': MAXIMUM_DOWNGRADE_CONFIG_VERSION + 1, 'OLDEST_COMPATIBLE': 0}}
102+
)
103+
82104

83105
def test_migrate_full(load_config_sample, monkeypatch):
84106
"""Test the full config migration."""

0 commit comments

Comments
 (0)