Skip to content
Draft
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
150 changes: 150 additions & 0 deletions src/aiida/cmdline/commands/cmd_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,153 @@
except Exception as e:
msg = f'Unexpected error during dump of {profile.name}:\n ({e!s}).\n'
echo.echo_critical(msg + traceback.format_exc())


@verdi_profile.command('from-backup')
@click.argument('path', type=click.Path(), nargs=1)
def profile_from_backup(path):
import shutil
import sqlite3
import subprocess
from pathlib import Path

Check warning on line 431 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L428-L431

Added lines #L428 - L431 were not covered by tests

from aiida.manage.configuration import create_profile
from aiida.manage.configuration.config import Config

Check warning on line 434 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L433-L434

Added lines #L433 - L434 were not covered by tests

def import_sqlite_database(backup_db_path, target_db_path):

Check warning on line 436 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L436

Added line #L436 was not covered by tests
"""Import data from backup SQLite database to target database using SQLite's backup API."""
print('Importing database content...')

Check warning on line 438 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L438

Added line #L438 was not covered by tests

# First, let's check if the target database is properly initialized
target_conn = sqlite3.connect(str(target_db_path))

Check warning on line 441 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L441

Added line #L441 was not covered by tests

# Check if the database has the alembic_version table (indicates proper initialization)
cursor = target_conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='alembic_version'")
has_alembic = cursor.fetchone() is not None
target_conn.close()

Check warning on line 446 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L444-L446

Added lines #L444 - L446 were not covered by tests

if not has_alembic:
print('Warning: Target database appears uninitialized. Using direct backup method.')

Check warning on line 449 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L448-L449

Added lines #L448 - L449 were not covered by tests
# If target is not initialized, just do a direct backup
backup_conn = sqlite3.connect(str(backup_db_path))
target_conn = sqlite3.connect(str(target_db_path))

Check warning on line 452 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L451-L452

Added lines #L451 - L452 were not covered by tests

try:
with target_conn:
backup_conn.backup(target_conn)
print('Database content imported successfully')

Check warning on line 457 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L454-L457

Added lines #L454 - L457 were not covered by tests
finally:
backup_conn.close()
target_conn.close()

Check warning on line 460 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L459-L460

Added lines #L459 - L460 were not covered by tests
else:
print('Target database is initialized. Importing data while preserving schema...')

Check warning on line 462 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L462

Added line #L462 was not covered by tests
# Target is initialized, so we need to import data more carefully
backup_conn = sqlite3.connect(str(backup_db_path))
target_conn = sqlite3.connect(str(target_db_path))

Check warning on line 465 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L464-L465

Added lines #L464 - L465 were not covered by tests

try:

Check warning on line 467 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L467

Added line #L467 was not covered by tests
# Attach the backup database
target_conn.execute(f"ATTACH '{backup_db_path}' AS backup_db")

Check warning on line 469 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L469

Added line #L469 was not covered by tests

# Get list of tables from backup (excluding schema management tables)
tables = backup_conn.execute("""

Check warning on line 472 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L472

Added line #L472 was not covered by tests
SELECT name FROM sqlite_master
WHERE type='table'
AND name NOT IN ('alembic_version', 'sqlite_sequence')
""").fetchall()

for (table_name,) in tables:

Check warning on line 478 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L478

Added line #L478 was not covered by tests
# Clear existing data
target_conn.execute(f'DELETE FROM {table_name}')

Check warning on line 480 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L480

Added line #L480 was not covered by tests

# Copy data from backup
target_conn.execute(f'INSERT INTO {table_name} SELECT * FROM backup_db.{table_name}')

Check warning on line 483 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L483

Added line #L483 was not covered by tests

target_conn.commit()
print('Database content imported successfully')

Check warning on line 486 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L485-L486

Added lines #L485 - L486 were not covered by tests

finally:
backup_conn.close()
target_conn.close()

Check warning on line 490 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L489-L490

Added lines #L489 - L490 were not covered by tests

def restore_repository(backup_container_path, target_container_path):

Check warning on line 492 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L492

Added line #L492 was not covered by tests
"""Restore the disk-objectstore container from backup."""
print('Restoring repository...')

Check warning on line 494 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L494

Added line #L494 was not covered by tests

if not backup_container_path.exists():
print('Warning: No container directory found in backup')
return

Check warning on line 498 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L496-L498

Added lines #L496 - L498 were not covered by tests

if target_container_path.exists():
shutil.rmtree(target_container_path)

Check warning on line 501 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L500-L501

Added lines #L500 - L501 were not covered by tests

# Use rsync for efficient copying
try:
subprocess.run(

Check warning on line 505 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L504-L505

Added lines #L504 - L505 were not covered by tests
['rsync', '-arvz', str(backup_container_path) + '/', str(target_container_path) + '/'], check=True
)
print('Repository restored successfully')
except (subprocess.CalledProcessError, FileNotFoundError):

Check warning on line 509 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L508-L509

Added lines #L508 - L509 were not covered by tests
# Fallback to shutil if rsync is not available
shutil.copytree(backup_container_path, target_container_path)
print('Repository restored using fallback method')

Check warning on line 512 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L511-L512

Added lines #L511 - L512 were not covered by tests

# Fix UUID mismatch between database and container
print('Synchronizing container UUID with database...')
try:
from disk_objectstore import Container

Check warning on line 517 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L515-L517

Added lines #L515 - L517 were not covered by tests

# Get the actual UUID from the restored container
container = Container(target_container_path)
actual_container_uuid = container.container_id

Check warning on line 521 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L520-L521

Added lines #L520 - L521 were not covered by tests

# Update the database to match the container UUID
target_db = target_container_path.parent / 'database.sqlite'
conn = sqlite3.connect(str(target_db))
try:

Check warning on line 526 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L524-L526

Added lines #L524 - L526 were not covered by tests
# Update the repository UUID in the database
conn.execute(

Check warning on line 528 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L528

Added line #L528 was not covered by tests
"UPDATE db_dbsetting SET val = ? WHERE key = 'repository|uuid'", (f'"{actual_container_uuid}"',)
)
conn.commit()
print(f'Updated database repository UUID to: {actual_container_uuid}')

Check warning on line 532 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L531-L532

Added lines #L531 - L532 were not covered by tests
finally:
conn.close()

Check warning on line 534 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L534

Added line #L534 was not covered by tests

except Exception as e:
print(f'Warning: Could not sync container UUID: {e}')
print("You may need to run 'verdi storage maintain' to fix UUID mismatch")

Check warning on line 538 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L536-L538

Added lines #L536 - L538 were not covered by tests

# Load backup configuration
backup_config = Config.from_file(Path(path) / 'config.json')
current_config = get_config()
backup_profile = backup_config.get_profile(name=next(iter(backup_config.dictionary['profiles'].keys())))

Check warning on line 543 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L541-L543

Added lines #L541 - L543 were not covered by tests

print(f'Restoring backup from profile: {backup_profile.name}')

Check warning on line 545 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L545

Added line #L545 was not covered by tests

# Create new profile with fresh storage
new_profile_name = f'{backup_profile.name}-restored'

Check warning on line 548 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L548

Added line #L548 was not covered by tests

restored_profile = create_profile(

Check warning on line 550 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L550

Added line #L550 was not covered by tests
current_config,
name=new_profile_name,
email=backup_profile.default_user_email,
storage_backend=backup_profile.storage_backend,
storage_config={}, # Fresh, empty storage config
)
current_config.add_profile(restored_profile)
current_config.store()

Check warning on line 558 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L557-L558

Added lines #L557 - L558 were not covered by tests

print(f'Created new profile: {new_profile_name}')

Check warning on line 560 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L560

Added line #L560 was not covered by tests

# Import database content
backup_db = Path(path) / 'database.sqlite'
target_db = Path(restored_profile.storage_config['filepath']) / 'database.sqlite'
import_sqlite_database(backup_db, target_db)

Check warning on line 565 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L563-L565

Added lines #L563 - L565 were not covered by tests

# Restore the repository
backup_container_path = Path(path) / 'last-backup' / 'container'
target_container_path = Path(restored_profile.storage_config['filepath']) / 'container'
restore_repository(backup_container_path, target_container_path)

Check warning on line 570 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L568-L570

Added lines #L568 - L570 were not covered by tests

print(f"Profile '{new_profile_name}' restored successfully!")

Check warning on line 572 in src/aiida/cmdline/commands/cmd_profile.py

View check run for this annotation

Codecov / codecov/patch

src/aiida/cmdline/commands/cmd_profile.py#L572

Added line #L572 was not covered by tests
Loading