Skip to content

Commit 6a8e2d7

Browse files
committed
👌 Allow config downgrade for known migrations (aiidateam#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.
1 parent 3e42724 commit 6a8e2d7

5 files changed

Lines changed: 124 additions & 28 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
@@ -181,9 +181,10 @@ class MissingConfigurationError(ConfigurationError):
181181

182182

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

188189

189190
class ClosedStorage(AiidaException):

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

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
CURRENT_CONFIG_VERSION = 10
3737
OLDEST_COMPATIBLE_CONFIG_VERSION = 10
38+
# Highest configuration version for which this code can run downgrade migrations, even if it cannot load it.
39+
MAXIMUM_DOWNGRADE_CONFIG_VERSION = 10
3840

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

@@ -498,6 +500,41 @@ def get_oldest_compatible_version(config):
498500
return config.get('CONFIG_VERSION', {}).get('OLDEST_COMPATIBLE', 0)
499501

500502

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

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

596649
return CURRENT_CONFIG_VERSION > current_version

‎tests/cmdline/commands/test_config.py‎

Lines changed: 26 additions & 10 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
@@ -274,15 +277,28 @@ def test_config_downgrade(run_cli_command, config_with_profile_factory):
274277
assert 'Success: Downgraded' in result.output.strip()
275278

276279

277-
@pytest.mark.presto
278-
def test_config_list_advanced_flag(run_cli_command, config_with_profile_factory):
279-
"""Test that ``verdi config list --advanced`` flag is accepted and footer hint is shown."""
280-
config_with_profile_factory()
280+
def test_config_downgrade_incompatible_but_downgradeable(run_cli_command, config_with_profile_factory, monkeypatch):
281+
"""Test `verdi config downgrade` can run even if the config cannot be loaded normally."""
282+
from aiida.manage import configuration
283+
from aiida.manage.configuration.migrations import migrations
284+
285+
config = config_with_profile_factory()
286+
filepath = pathlib.Path(config.filepath)
287+
dictionary = config.dictionary
288+
dictionary['CONFIG_VERSION'] = {'CURRENT': 10, 'OLDEST_COMPATIBLE': 10}
289+
dictionary.setdefault('options', {})['broker.task_timeout'] = 12
290+
filepath.write_text(json.dumps(dictionary), encoding='utf8')
291+
292+
configuration.CONFIG = None
293+
monkeypatch.setattr(migrations, 'CURRENT_CONFIG_VERSION', 9)
294+
monkeypatch.setattr(migrations, 'MAXIMUM_DOWNGRADE_CONFIG_VERSION', 10)
281295

282-
result_default = run_cli_command(cmd_verdi.verdi, ['config', 'list'], use_subprocess=False)
283-
assert '--advanced' in result_default.output
284-
assert 'logging.sqlalchemy_loglevel' not in result_default.output
296+
result = run_cli_command(
297+
cmd_verdi.verdi, ['config', 'downgrade', '9'], initialize_ctx_obj=False, use_subprocess=False
298+
)
285299

286-
result_advanced = run_cli_command(cmd_verdi.verdi, ['config', 'list', '--advanced'], use_subprocess=False)
287-
assert 'Use `verdi config list --advanced`' not in result_advanced.output
288-
assert 'logging.db_loglevel' not in result_advanced.output
300+
assert 'Success: Downgraded' in result.output.strip()
301+
dictionary = json.loads(filepath.read_text(encoding='utf8'))
302+
assert dictionary['CONFIG_VERSION'] == {'CURRENT': 9, 'OLDEST_COMPATIBLE': 9}
303+
assert dictionary['options']['rmq.task_timeout'] == 12
304+
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)