db_ops.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. """CLI commands for saving, resetting, etc of the database"""
  2. from datetime import datetime
  3. import subprocess
  4. from flask import current_app as app
  5. from flask.cli import with_appcontext
  6. import flask_migrate as migrate
  7. import click
  8. from flexmeasures.cli.utils import MsgStyle
  9. BACKUP_PATH: str = app.config.get("FLEXMEASURES_DB_BACKUP_PATH") # type: ignore
  10. @click.group("db-ops")
  11. def fm_db_ops():
  12. """FlexMeasures: Reset, Dump/Restore or Save/Load the DB data."""
  13. @fm_db_ops.command()
  14. @with_appcontext
  15. def reset():
  16. """Reset database data and re-create tables from data model."""
  17. if not app.debug:
  18. prompt = (
  19. "This deletes all data and re-creates the tables on %s.\nDo you want to continue?"
  20. % app.db.engine
  21. )
  22. if not click.confirm(prompt):
  23. click.secho("I did nothing.", **MsgStyle.WARN)
  24. raise click.Abort()
  25. from flexmeasures.data.scripts.data_gen import reset_db
  26. current_version = migrate.current()
  27. reset_db(app.db)
  28. migrate.stamp(current_version)
  29. @fm_db_ops.command()
  30. @with_appcontext
  31. @click.option("--name", help="Unique name for saving the backup.")
  32. @click.option("--dir", default=BACKUP_PATH, help="Directory for saving backups.")
  33. @click.option(
  34. "--structure/--no-structure",
  35. default=True,
  36. help="Save structural data like asset (types), market (types),"
  37. " weather (sensors), users, roles.",
  38. )
  39. @click.option(
  40. "--data/--no-data",
  41. default=False,
  42. help="Save (time series) data to a backup. Only do this for small data sets!",
  43. )
  44. def save(name: str, dir: str = BACKUP_PATH, structure: bool = True, data: bool = False):
  45. """Backup db content to files."""
  46. if name:
  47. from flexmeasures.data.scripts.data_gen import save_tables
  48. save_tables(app.db, name, structure=structure, data=data, backup_path=dir)
  49. else:
  50. click.secho(
  51. "You must specify a unique name for the backup: --name <unique name>",
  52. **MsgStyle.ERROR,
  53. )
  54. @fm_db_ops.command()
  55. @with_appcontext
  56. @click.option("--name", help="Name of the backup.")
  57. @click.option("--dir", default=BACKUP_PATH, help="Directory for loading backups.")
  58. @click.option(
  59. "--structure/--no-structure",
  60. default=True,
  61. help="Load structural data like asset (types), market (types),"
  62. " weather (sensors), users, roles.",
  63. )
  64. @click.option("--data/--no-data", default=False, help="Load (time series) data.")
  65. def load(name: str, dir: str = BACKUP_PATH, structure: bool = True, data: bool = False):
  66. """Load backed-up contents (see `db-ops save`), run `reset` first."""
  67. if name:
  68. from flexmeasures.data.scripts.data_gen import load_tables
  69. load_tables(app.db, name, structure=structure, data=data, backup_path=dir)
  70. else:
  71. click.secho(
  72. "You must specify the name of the backup: --name <unique name>",
  73. **MsgStyle.ERROR,
  74. )
  75. @fm_db_ops.command()
  76. @with_appcontext
  77. def dump():
  78. """
  79. Create a dump of all current data (using `pg_dump`).
  80. If you have a version mismatch between server and client, here is an alternative:
  81. $ docker run --pull=always -it postgres:15.7 bash # use server version here
  82. $ docker exec -it <container> <the pg_dump command (see code)>
  83. $ docker cp <container>:<your-dump-filename> .
  84. $ docker stop <container>; docker rm <container>
  85. """
  86. db_uri = app.config.get("SQLALCHEMY_DATABASE_URI")
  87. db_host_and_db_name = db_uri.split("@")[-1]
  88. click.echo(f"Backing up {db_host_and_db_name} database")
  89. db_name = db_host_and_db_name.split("/")[-1]
  90. time_of_saving = datetime.now().strftime("%F-%H%M")
  91. dump_filename = f"pgbackup_{db_name}_{time_of_saving}.dump"
  92. command_for_dumping = f"pg_dump --no-privileges --no-owner --data-only --format=c --file={dump_filename} '{db_uri}'"
  93. try:
  94. subprocess.check_output(command_for_dumping, shell=True)
  95. click.secho(f"db dump successful: saved to {dump_filename}", **MsgStyle.SUCCESS)
  96. except Exception as e:
  97. click.secho(f"Exception happened during dump: {e}", **MsgStyle.ERROR)
  98. click.secho("db dump unsuccessful", **MsgStyle.ERROR)
  99. @fm_db_ops.command()
  100. @with_appcontext
  101. @click.argument("file", type=click.Path(exists=True))
  102. def restore(file: str):
  103. """Restore the dump file, see `db-ops dump` (run `reset` first).
  104. From the command line:
  105. % flexmeasures db-ops dump
  106. % flexmeasures db-ops reset
  107. % flexmeasures db-ops restore FILE
  108. """
  109. db_uri: str = app.config.get("SQLALCHEMY_DATABASE_URI") # type: ignore
  110. db_host_and_db_name = db_uri.split("@")[-1]
  111. click.echo(f"Restoring {db_host_and_db_name} database from file {file}")
  112. command_for_restoring = f"pg_restore -d {db_uri} {file}"
  113. try:
  114. subprocess.check_output(command_for_restoring, shell=True)
  115. click.secho("db restore successful", **MsgStyle.SUCCESS)
  116. except Exception as e:
  117. click.secho(f"Exception happened during restore: {e}", **MsgStyle.ERROR)
  118. click.secho("db restore unsuccessful", **MsgStyle.ERROR)
  119. app.cli.add_command(fm_db_ops)