lsup_admin.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. import click
  2. import click_log
  3. import csv
  4. import json
  5. import logging
  6. import sys
  7. from os import getcwd, path
  8. import arrow
  9. from lakesuperior import env
  10. # Do not set up environment yet. Some methods need special setup folders.
  11. # As a consequence, API modules must be imported inside each function after
  12. # setup.
  13. from lakesuperior.exceptions import ChecksumValidationError
  14. __doc__="""
  15. Utility to perform core maintenance tasks via console command-line.
  16. The command-line tool is self-documented. Type::
  17. lsup-admin --help
  18. for a list of tools and options.
  19. """
  20. logger = logging.getLogger(__name__)
  21. click_log.basic_config(logger)
  22. @click.group()
  23. def admin():
  24. pass
  25. @click.command()
  26. def bootstrap():
  27. """
  28. Bootstrap binary and graph stores.
  29. This script will parse configuration files and initialize a filesystem and
  30. triplestore with an empty FCREPO repository.
  31. It is used in test suites and on a first run.
  32. Additional scaffolding files may be parsed to create initial contents.
  33. """
  34. env.setup()
  35. rdfly = env.app_globals.rdfly
  36. nonrdfly = env.app_globals.nonrdfly
  37. click.echo(
  38. click.style(
  39. 'WARNING: This operation will WIPE ALL YOUR DATA.\n',
  40. bold=True, fg='red')
  41. + 'Are you sure? (Please type `yes` to continue) > ', nl=False)
  42. choice = input().lower()
  43. if choice != 'yes':
  44. click.echo('Aborting.')
  45. sys.exit(1)
  46. click.echo('Initializing graph store at {}'.format(rdfly.store.env_path))
  47. rdfly.bootstrap()
  48. click.echo('Graph store initialized.')
  49. click.echo('Initializing binary store at {}'.format(nonrdfly.root))
  50. nonrdfly.bootstrap()
  51. click.echo('Binary store initialized.')
  52. click.echo('\nRepository successfully set up. Go to town.')
  53. click.echo('If the HTTP server is running, it must be restarted.')
  54. @click.command()
  55. @click.option(
  56. '--human', '-h', is_flag=True, flag_value=True,
  57. help='Print a human-readable string. By default, JSON is printed.')
  58. def stats(human=False):
  59. """
  60. Print repository statistics.
  61. @param human (bool) Whether to output the data in human-readable
  62. format.
  63. """
  64. env.setup()
  65. from lakesuperior.api import admin as admin_api
  66. stat_data = admin_api.stats()
  67. if human:
  68. click.echo(
  69. 'This option is not supported yet. Sorry.\nUse the `/admin/stats`'
  70. ' endpoint in the web UI for a pretty printout.')
  71. else:
  72. click.echo(json.dumps(stat_data))
  73. @click.command()
  74. @click.argument('uid')
  75. def check_fixity(uid):
  76. """
  77. Check the fixity of a resource.
  78. """
  79. env.setup()
  80. from lakesuperior.api import admin as admin_api
  81. try:
  82. admin_api.fixity_check(uid)
  83. except ChecksumValidationError:
  84. print(f'Checksum for {uid} failed.')
  85. sys.exit(1)
  86. print(f'Checksum for {uid} passed.')
  87. @click.option(
  88. '--config-folder', '-c', default=None, help='Alternative configuration '
  89. 'folder to look up. If not set, the location set in the environment or '
  90. 'the default configuration is used.')
  91. @click.option(
  92. '--output', '-o', default=None, help='Output file. If not specified, a '
  93. 'timestamp-named file will be generated automatically.')
  94. @click.command()
  95. def check_refint(config_folder=None, output=None):
  96. """
  97. Check referential integrity.
  98. This command scans the graph store to verify that all references to
  99. resources within the repository are effectively pointing to existing
  100. resources. For repositories set up with the `referential_integrity` option
  101. (the default), this is a pre-condition for a consistent data set.
  102. If inconsistencies are found, a report is generated in CSV format with the
  103. following columns: `s`, `p`, `o` (respectively the terms of the
  104. triple containing the dangling relationship) and `missing` which
  105. indicates which term is the missing URI (currently always set to `o`).
  106. Note: this check can be run regardless of whether the repository enforces
  107. referential integrity.
  108. """
  109. env.setup(config_folder)
  110. from lakesuperior.api import admin as admin_api
  111. check_results = admin_api.integrity_check()
  112. click.echo('Integrity check results:')
  113. if len(check_results):
  114. click.echo(click.style('Inconsistencies found!', fg='red', bold=True))
  115. if not output:
  116. output = path.join(getcwd(), 'refint_report-{}.csv'.format(
  117. arrow.utcnow().format('YYYY-MM-DDTHH:mm:ss.S')))
  118. elif not output.endswith('.csv'):
  119. output += '.csv'
  120. with open(output, 'w', newline='') as fh:
  121. writer = csv.writer(fh)
  122. writer.writerow(('s', 'p', 'o', 'missing'))
  123. for trp in check_results:
  124. # ``o`` is always hardcoded for now.
  125. writer.writerow([t.n3() for t in trp[0]] + ['o'])
  126. click.echo('Report generated at {}'.format(output))
  127. else:
  128. click.echo(click.style('Clean. ', fg='green', bold=True)
  129. + 'No inconsistency found. No report generated.')
  130. @click.command()
  131. def cleanup():
  132. """
  133. [STUB] Clean up orphan database items.
  134. """
  135. pass
  136. @click.command()
  137. @click.argument('src')
  138. @click.argument('dest')
  139. @click.option(
  140. '--auth', '-a',
  141. help='Colon-separated credentials for HTTP Basic authentication on the '
  142. 'source repository. E.g. `user:password`.')
  143. @click.option(
  144. '--start', '-s', show_default=True,
  145. help='Starting point for looking for resources in the repository.\n'
  146. 'The default `/` value starts at the root, i.e. migrates the whole '
  147. 'repository.')
  148. @click.option(
  149. '--list-file', '-l', help='Path to a local file containing URIs to be '
  150. 'used as starting points, one per line. Use this alternatively to `-s`. '
  151. 'The URIs can be relative to the repository root (e.g. `/a/b/c`) or fully '
  152. 'qualified (e.g. `https://example.edu/fcrepo/rest/a/b/c`).')
  153. @click.option(
  154. '--zero-binaries', '-z', is_flag=True,
  155. help='If set, binaries are created as zero-byte files in the proper '
  156. 'folder structure rather than having their full content copied.')
  157. @click.option(
  158. '--skip-errors', '-e', is_flag=True,
  159. help='If set, when the application encounters an error while retrieving '
  160. 'a resource from the source repository, it will log the error rather than '
  161. 'quitting. Other exceptions caused by the application will terminate the '
  162. 'process as usual.')
  163. @click_log.simple_verbosity_option(logger)
  164. def migrate(src, dest, auth, start, list_file, zero_binaries, skip_errors):
  165. """
  166. Migrate an LDP repository to Lakesuperior.
  167. This utility creates a fully functional Lakesuperior repository from an
  168. existing repository. The source repo can be Lakesuperior or
  169. another LDP-compatible implementation.
  170. A folder will be created in the location indicated by ``dest``. If the
  171. folder exists already, it will be deleted and recreated. The folder will be
  172. populated with the RDF and binary data directories and a default
  173. configuration directory. The new repository can be immediately started
  174. from this location.
  175. """
  176. env.setup()
  177. from lakesuperior.api import admin as admin_api
  178. logger.info('Migrating {} into a new repository on {}.'.format(
  179. src, dest))
  180. src_auth = tuple(auth.split(':')) if auth else None
  181. entries = admin_api.migrate(
  182. src, dest, src_auth=src_auth, start_pts=start, list_file=list_file,
  183. zero_binaries=zero_binaries, skip_errors=skip_errors)
  184. logger.info('Migrated {} resources.'.format(entries))
  185. logger.info(f'''
  186. Migration complete. A new Lakesuperior environment has been created in
  187. {dest}. To start the new repository, run:
  188. FCREPO_CONFIG_DIR="{dest}/etc" fcrepo
  189. from the directory you launched this script in.
  190. Make sure that the default port is not being used by another repository.
  191. ''')
  192. admin.add_command(bootstrap)
  193. admin.add_command(check_fixity)
  194. admin.add_command(check_refint)
  195. admin.add_command(cleanup)
  196. admin.add_command(migrate)
  197. admin.add_command(stats)
  198. if __name__ == '__main__':
  199. admin()