RWSHelper.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/env python3
  2. # Contest Management System - http://cms-dev.github.io/
  3. # Copyright © 2013 Luca Wehrstedt <luca.wehrstedt@gmail.com>
  4. # Copyright © 2016 Stefano Maggiolo <s.maggiolo@gmail.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as
  8. # published by the Free Software Foundation, either version 3 of the
  9. # License, or (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """A script to interact with RWSs using HTTP requests
  19. Provide a handy command-line interface to do common operations on
  20. entities stored on RankingWebServers. Particularly useful to delete an
  21. entity that has been deleted in the DB without any downtime.
  22. """
  23. # We enable monkey patching to make many libraries gevent-friendly
  24. # (for instance, urllib3, used by requests)
  25. import gevent.monkey
  26. gevent.monkey.patch_all() # noqa
  27. import argparse
  28. import logging
  29. import sys
  30. from urllib.parse import quote, urljoin, urlsplit
  31. from requests import Session, Request
  32. from requests.exceptions import RequestException
  33. from cms import config, utf8_decoder
  34. logger = logging.getLogger(__name__)
  35. ACTION_METHODS = {
  36. 'get': 'GET',
  37. 'create': 'PUT', # Create is actually an update.
  38. 'update': 'PUT',
  39. 'delete': 'DELETE',
  40. }
  41. ENTITY_TYPES = ['contest',
  42. 'task',
  43. 'team',
  44. 'user',
  45. 'submission',
  46. 'subchange',
  47. ]
  48. def get_url(shard, entity_type, entity_id):
  49. return urljoin(config.rankings[shard], '%ss/%s' % (entity_type, entity_id))
  50. def main():
  51. parser = argparse.ArgumentParser(prog='cmsRWSHelper')
  52. parser.add_argument(
  53. '-v', '--verbose', action='store_true',
  54. help="tell on stderr what's happening")
  55. # FIXME It would be nice to use '--rankings' with action='store'
  56. # and nargs='+' but it doesn't seem to work with subparsers...
  57. parser.add_argument(
  58. '-r', '--ranking', dest='rankings', action='append', type=int,
  59. choices=list(range(len(config.rankings))), metavar='shard',
  60. help="select which RWS to connect to (omit for 'all')")
  61. subparsers = parser.add_subparsers(
  62. title='available actions', metavar='action',
  63. help='what to ask the RWS to do with the entity')
  64. # Create the parser for the "get" command
  65. parser_get = subparsers.add_parser('get', help="retrieve the entity")
  66. parser_get.set_defaults(action='get')
  67. # Create the parser for the "create" command
  68. parser_create = subparsers.add_parser('create', help="create the entity")
  69. parser_create.set_defaults(action='create')
  70. parser_create.add_argument(
  71. 'file', action="store", type=argparse.FileType('rb'),
  72. help="file holding the entity body to send ('-' for stdin)")
  73. # Create the parser for the "update" command
  74. parser_update = subparsers.add_parser('update', help='update the entity')
  75. parser_update.set_defaults(action='update')
  76. parser_update.add_argument(
  77. 'file', action="store", type=argparse.FileType('rb'),
  78. help="file holding the entity body to send ('-' for stdin)")
  79. # Create the parser for the "delete" command
  80. parser_delete = subparsers.add_parser('delete', help='delete the entity')
  81. parser_delete.set_defaults(action='delete')
  82. # Create the group for entity-related arguments
  83. group = parser.add_argument_group(
  84. title='entity reference')
  85. group.add_argument(
  86. 'entity_type', action='store', choices=ENTITY_TYPES, metavar='type',
  87. help="type of the entity (e.g. contest, user, task, etc.)")
  88. group.add_argument(
  89. 'entity_id', action='store', type=utf8_decoder, metavar='id',
  90. help='ID of the entity (usually a short codename)')
  91. # Parse the given arguments
  92. args = parser.parse_args()
  93. args.entity_id = quote(args.entity_id)
  94. if args.verbose:
  95. verb = args.action[:4] + 'ting'
  96. logger.info("%s entity '%ss/%s'", verb.capitalize(),
  97. args.entity_type, args.entity_id)
  98. if args.rankings is not None:
  99. shards = args.rankings
  100. else:
  101. shards = list(range(len(config.rankings)))
  102. s = Session()
  103. had_error = False
  104. for shard in shards:
  105. url = get_url(shard, args.entity_type, args.entity_id)
  106. # XXX With requests-1.2 auth is automatically extracted from
  107. # the URL: there is no need for this.
  108. auth = urlsplit(url)
  109. if args.verbose:
  110. logger.info("Preparing %s request to %s",
  111. ACTION_METHODS[args.action], url)
  112. if hasattr(args, 'file'):
  113. if args.verbose:
  114. logger.info("Reading file contents to use as message body")
  115. body = args.file.read()
  116. else:
  117. body = None
  118. req = Request(ACTION_METHODS[args.action], url, data=body,
  119. auth=(auth.username, auth.password),
  120. headers={'content-type': 'application/json'}).prepare()
  121. if args.verbose:
  122. logger.info("Sending request")
  123. try:
  124. res = s.send(req, verify=config.https_certfile)
  125. except RequestException:
  126. logger.error("Failed", exc_info=True)
  127. had_error = True
  128. continue
  129. if args.verbose:
  130. logger.info("Response received")
  131. if 400 <= res.status_code < 600:
  132. logger.error("Unexpected status code: %d", res.status_code)
  133. had_error = True
  134. continue
  135. if args.action == "get":
  136. print(res.content)
  137. if had_error:
  138. return 1
  139. else:
  140. return 0
  141. if __name__ == "__main__":
  142. sys.exit(main())