Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create backup of configuration file before migrating #3568

Merged
merged 1 commit into from
Nov 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 11 additions & 13 deletions aiida/manage/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,7 @@ def load_config(create=False):
import io
import os
from aiida.common import exceptions
from aiida.common import json
from .config import Config
from .migrations import check_and_migrate_config
from .settings import AIIDA_CONFIG_FOLDER, DEFAULT_CONFIG_FILE_NAME

if IN_RT_DOC_MODE:
Expand Down Expand Up @@ -114,19 +112,19 @@ def load_config(create=False):

filepath = os.path.join(AIIDA_CONFIG_FOLDER, DEFAULT_CONFIG_FILE_NAME)

if not os.path.isfile(filepath) and not create:
raise exceptions.MissingConfigurationError('configuration file {} does not exist'.format(filepath))

# If it doesn't exist, just create the file
if not os.path.isfile(filepath):
if not create:
raise exceptions.MissingConfigurationError('configuration file {} does not exist'.format(filepath))
else:
config_dictionary = {}
else:
try:
with io.open(filepath, 'r', encoding='utf8') as handle:
config_dictionary = json.load(handle)
except ValueError:
raise exceptions.ConfigurationError('configuration file {} contains invalid JSON'.format(filepath))
from aiida.common import json
with io.open(filepath, 'ab') as handle:
json.dump({}, handle)

config = Config(filepath, check_and_migrate_config(config_dictionary))
try:
config = Config.from_file(filepath)
except ValueError:
raise exceptions.ConfigurationError('configuration file {} contains invalid JSON'.format(filepath))
config.store()

return config
Expand Down
72 changes: 52 additions & 20 deletions aiida/manage/configuration/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

from aiida.common import json

from .migrations import CURRENT_CONFIG_VERSION, OLDEST_COMPATIBLE_CONFIG_VERSION
from .options import get_option, parse_option, NO_DEFAULT
from .profile import Profile
from .settings import DEFAULT_UMASK, DEFAULT_CONFIG_INDENT_SIZE
Expand All @@ -36,6 +35,52 @@ class Config(object): # pylint: disable=too-many-public-methods
KEY_PROFILES = 'profiles'
KEY_OPTIONS = 'options'

@classmethod
def from_file(cls, filepath):
"""Instantiate a configuration object from the contents of a given file.

:param filepath: the absolute path to the configuration file
:return: `Config` instance
"""
from aiida.cmdline.utils import echo
from .migrations import check_and_migrate_config, config_needs_migrating

with io.open(filepath, 'r', encoding='utf8') as handle:
config = json.load(handle)

# If the configuration file needs to be migrated, first create a specific backup so it can easily be reverted
if config_needs_migrating(config):
echo.echo_warning('current configuration file `{}` is outdated and will be migrated'.format(filepath))
filepath_backup = cls._backup(filepath)
echo.echo_warning('original backed up to `{}`'.format(filepath_backup))

config = check_and_migrate_config(config)

return Config(filepath, config)

@classmethod
def _backup(cls, filepath):
"""Create a backup of the configuration file with the given filepath.

:param filepath: absolute path to the configuration file to backup
:return: the absolute path of the created backup
"""
from aiida.common import timezone

filepath_backup = None

# Keep generating a new backup filename based on the current time until it does not exist
while not filepath_backup or os.path.isfile(filepath_backup):
filepath_backup = '{}.{}'.format(filepath, timezone.now().strftime('%Y%m%d-%H%M%S.%f'))

try:
umask = os.umask(DEFAULT_UMASK)
shutil.copy(filepath, filepath_backup)
finally:
os.umask(umask)

return filepath_backup

def __init__(self, filepath, config):
"""Instantiate a configuration object from a configuration dictionary and its filepath.

Expand All @@ -44,6 +89,8 @@ def __init__(self, filepath, config):
:param filepath: the absolute filepath of the configuration file
:param config: the content of the configuration file in dictionary form
"""
from .migrations import CURRENT_CONFIG_VERSION, OLDEST_COMPATIBLE_CONFIG_VERSION

version = config.get(self.KEY_VERSION, {})
current_version = version.get(self.KEY_VERSION_CURRENT, CURRENT_CONFIG_VERSION)
compatible_version = version.get(self.KEY_VERSION_OLDEST_COMPATIBLE, OLDEST_COMPATIBLE_CONFIG_VERSION)
Expand Down Expand Up @@ -91,10 +138,9 @@ def handle_invalid(self, message):
:param message: a string message to echo with describing the infraction
"""
from aiida.cmdline.utils import echo
filepath = self._filepath + '.bak'
self._backup(filepath)
filepath_backup = self._backup(self.filepath)
echo.echo_warning(message)
echo.echo_warning('backup of the original config file written to: `{}`'.format(filepath))
echo.echo_warning('backup of the original config file written to: `{}`'.format(filepath_backup))

@property
def dictionary(self):
Expand Down Expand Up @@ -321,7 +367,8 @@ def get_option(self, option_name, scope=None, default=True):

def store(self):
"""Write the current config to file."""
self._backup()
if os.path.isfile(self.filepath):
self._backup(self.filepath)

umask = os.umask(DEFAULT_UMASK)

Expand All @@ -332,18 +379,3 @@ def store(self):
os.umask(umask)

return self

def _backup(self, filepath=None):
"""Create a backup of the current config as it exists on disk."""
if not os.path.isfile(self.filepath):
return

umask = os.umask(DEFAULT_UMASK)

if filepath is None:
filepath = self.filepath + '~'

try:
shutil.copy(self.filepath, filepath)
finally:
os.umask(umask)
4 changes: 2 additions & 2 deletions aiida/manage/configuration/migrations/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from aiida.common import exceptions
from .migrations import _MIGRATION_LOOKUP, CURRENT_CONFIG_VERSION

__all__ = ('check_and_migrate_config',)
__all__ = ('check_and_migrate_config', 'config_needs_migrating', 'get_current_version')


def check_and_migrate_config(config):
Expand All @@ -37,7 +37,7 @@ def config_needs_migrating(config):
in the code, the config cannot be used and so the function will raise.

:return: True if the configuration has an older version and needs to be migrated, False otherwise
:raises ConfigurationVersionError: if the oldest compatible version of the config is higher than the current.
:raises aiida.common.ConfigurationVersionError: if the config's oldest compatible version is higher than the current
"""
current_version = get_current_version(config)
oldest_compatible_version = get_oldest_compatible_version(config)
Expand Down