app.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #!/usr/bin/env python3
  2. import os
  3. import base64
  4. from flask import Flask, render_template, request, send_from_directory, jsonify, redirect
  5. from threading import Thread
  6. import sys
  7. import requests
  8. from logging.config import dictConfig
  9. dictConfig({
  10. 'version': 1,
  11. 'formatters': {'default': {
  12. 'format': '[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
  13. }},
  14. 'handlers': {'wsgi': {
  15. 'class': 'logging.StreamHandler',
  16. 'stream': 'ext://flask.logging.wsgi_errors_stream',
  17. 'formatter': 'default'
  18. }},
  19. 'root': {
  20. 'level': os.getenv('CBS_LOG_LEVEL', default='INFO'),
  21. 'handlers': ['wsgi']
  22. }
  23. })
  24. # let app.py know about the modules in the parent directory
  25. sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
  26. import ap_git
  27. import metadata_manager
  28. import build_manager
  29. from builder import Builder
  30. # run at lower priority
  31. os.nice(20)
  32. import optparse
  33. parser = optparse.OptionParser("app.py")
  34. parser.add_option("", "--basedir", type="string",
  35. default=os.getenv(
  36. key="CBS_BASEDIR",
  37. default=os.path.abspath(os.path.join(os.path.dirname(__file__),"..","base"))
  38. ),
  39. help="base directory")
  40. cmd_opts, cmd_args = parser.parse_args()
  41. # define directories
  42. basedir = os.path.abspath(cmd_opts.basedir)
  43. sourcedir = os.path.join(basedir, 'ardupilot')
  44. outdir_parent = os.path.join(basedir, 'builds')
  45. workdir_parent = os.path.join(basedir, 'workdir')
  46. appdir = os.path.dirname(__file__)
  47. builds_dict = {}
  48. REMOTES = None
  49. repo = ap_git.GitRepo.clone_if_needed(
  50. source="https://github.com/ardupilot/ardupilot.git",
  51. dest=sourcedir,
  52. recurse_submodules=True,
  53. )
  54. ap_src_metadata_fetcher = metadata_manager.APSourceMetadataFetcher(
  55. ap_repo=repo
  56. )
  57. versions_fetcher = metadata_manager.VersionsFetcher(
  58. remotes_json_path=os.path.join(basedir, 'configs', 'remotes.json'),
  59. ap_repo=repo
  60. )
  61. manager = build_manager.BuildManager(
  62. outdir=outdir_parent,
  63. redis_host=os.getenv('CBS_REDIS_HOST', default='localhost'),
  64. redis_port=os.getenv('CBS_REDIS_PORT', default='6379')
  65. )
  66. cleaner = build_manager.BuildArtifactsCleaner()
  67. progress_updater = build_manager.BuildProgressUpdater()
  68. versions_fetcher.start()
  69. cleaner.start()
  70. progress_updater.start()
  71. if os.getenv('CBS_ENABLE_INBUILT_BUILDER', default='1') == '1':
  72. builder = Builder(
  73. workdir=workdir_parent,
  74. source_repo=repo
  75. )
  76. builder_thread = Thread(
  77. target=builder.run,
  78. daemon=True
  79. )
  80. builder_thread.start()
  81. app = Flask(__name__, template_folder='templates')
  82. versions_fetcher.reload_remotes_json()
  83. app.logger.info('Python version is: %s' % sys.version)
  84. def get_auth_token():
  85. try:
  86. # try to read the secret token from the file
  87. with open(os.path.join(basedir, 'secrets', 'reload_token'), 'r') as file:
  88. token = file.read().strip()
  89. return token
  90. except (FileNotFoundError, PermissionError):
  91. app.logger.error("Couldn't open token file. Checking environment for token.")
  92. # if the file does not exist, check the environment variable
  93. return os.getenv('CBS_REMOTES_RELOAD_TOKEN')
  94. @app.route('/refresh_remotes', methods=['POST'])
  95. def refresh_remotes():
  96. auth_token = get_auth_token()
  97. if auth_token is None:
  98. app.logger.error("Couldn't retrieve authorization token")
  99. return "Internal Server Error", 500
  100. token = request.get_json().get('token')
  101. if not token or token != auth_token:
  102. return "Unauthorized", 401
  103. versions_fetcher.reload_remotes_json()
  104. return "Successfully refreshed remotes", 200
  105. @app.route('/generate', methods=['GET', 'POST'])
  106. def generate():
  107. try:
  108. version = request.form['version']
  109. remote_name, commit_ref = version.split('/', 1)
  110. remote_info = versions_fetcher.get_remote_info(remote_name)
  111. if remote_info is None:
  112. raise Exception(f"Remote {remote_name} is not whitelisted.")
  113. vehicle = request.form['vehicle']
  114. version_info = versions_fetcher.get_version_info(
  115. vehicle=vehicle,
  116. remote=remote_name,
  117. commit_ref=commit_ref
  118. )
  119. if version_info is None:
  120. raise Exception("Commit reference invalid or not listed to be built for given vehicle for remote")
  121. board = request.form['board']
  122. boards_at_commit = ap_src_metadata_fetcher.get_boards_at_commit(
  123. remote=remote_name,
  124. commit_ref=commit_ref
  125. )
  126. if board not in boards_at_commit:
  127. raise Exception("bad board")
  128. all_features = ap_src_metadata_fetcher.get_build_options_at_commit(
  129. remote=remote_name,
  130. commit_ref=commit_ref
  131. )
  132. chosen_defines = {
  133. feature.define
  134. for feature in all_features
  135. if request.form.get(feature.label) == "1"
  136. }
  137. git_hash = repo.commit_id_for_remote_ref(
  138. remote=remote_name,
  139. commit_ref=commit_ref
  140. )
  141. build_info = build_manager.BuildInfo(
  142. vehicle=vehicle,
  143. remote_info=remote_info,
  144. git_hash=git_hash,
  145. board=board,
  146. selected_features=chosen_defines
  147. )
  148. forwarded_for = request.headers.get('X-Forwarded-For', None)
  149. if forwarded_for:
  150. client_ip = forwarded_for.split(',')[0].strip()
  151. else:
  152. client_ip = request.remote_addr
  153. build_id = manager.submit_build(
  154. build_info=build_info,
  155. client_ip=client_ip,
  156. )
  157. app.logger.info('Redirecting to /viewlog')
  158. return redirect('/viewlog/'+build_id)
  159. except Exception as ex:
  160. app.logger.error(ex)
  161. return render_template('error.html', ex=ex)
  162. @app.route('/add_build')
  163. def add_build():
  164. app.logger.info('Rendering add_build.html')
  165. return render_template('add_build.html')
  166. def filter_build_options_by_category(build_options, category):
  167. return sorted([f for f in build_options if f.category == category], key=lambda x: x.description.lower())
  168. def parse_build_categories(build_options):
  169. return sorted(list(set([f.category for f in build_options])))
  170. @app.route('/', defaults={'token': None}, methods=['GET'])
  171. @app.route('/viewlog/<token>', methods=['GET'])
  172. def home(token):
  173. if token:
  174. app.logger.info("Showing log for build id " + token)
  175. app.logger.info('Rendering index.html')
  176. return render_template('index.html', token=token)
  177. @app.route("/builds/<path:name>")
  178. def download_file(name):
  179. app.logger.info('Downloading %s' % name)
  180. return send_from_directory(os.path.join(basedir,'builds'), name, as_attachment=False)
  181. @app.route("/boards_and_features/<string:vehicle_name>/<string:remote_name>/<string:commit_reference>", methods=['GET'])
  182. def boards_and_features(vehicle_name, remote_name, commit_reference):
  183. commit_reference = base64.urlsafe_b64decode(commit_reference).decode()
  184. if not versions_fetcher.is_version_listed(vehicle=vehicle_name, remote=remote_name, commit_ref=commit_reference):
  185. return "Bad request. Commit reference not allowed to build for the vehicle.", 400
  186. app.logger.info('Board list and build options requested for %s %s %s' % (vehicle_name, remote_name, commit_reference))
  187. # getting board list for the branch
  188. with repo.get_checkout_lock():
  189. boards = ap_src_metadata_fetcher.get_boards_at_commit(
  190. remote=remote_name,
  191. commit_ref=commit_reference
  192. )
  193. options = ap_src_metadata_fetcher.get_build_options_at_commit(
  194. remote=remote_name,
  195. commit_ref=commit_reference
  196. ) # this is a list of Feature() objects defined in build_options.py
  197. # parse the set of categories from these objects
  198. categories = parse_build_categories(options)
  199. features = []
  200. for category in categories:
  201. filtered_options = filter_build_options_by_category(options, category)
  202. category_options = [] # options belonging to a given category
  203. for option in filtered_options:
  204. category_options.append({
  205. 'label' : option.label,
  206. 'description' : option.description,
  207. 'default' : option.default,
  208. 'define' : option.define,
  209. 'dependency' : option.dependency,
  210. })
  211. features.append({
  212. 'name' : category,
  213. 'options' : category_options,
  214. })
  215. # creating result dictionary
  216. result = {
  217. 'boards' : boards,
  218. 'default_board' : boards[0],
  219. 'features' : features,
  220. }
  221. # return jsonified result dict
  222. return jsonify(result)
  223. @app.route("/get_versions/<string:vehicle_name>", methods=['GET'])
  224. def get_versions(vehicle_name):
  225. versions = list()
  226. for version_info in versions_fetcher.get_versions_for_vehicle(vehicle_name=vehicle_name):
  227. if version_info.release_type == "latest":
  228. title = f"Latest ({version_info.remote})"
  229. else:
  230. title = f"{version_info.release_type} {version_info.version_number} ({version_info.remote})"
  231. id = f"{version_info.remote}/{version_info.commit_ref}"
  232. versions.append({
  233. "title" : title,
  234. "id" : id,
  235. })
  236. return jsonify(sorted(versions, key=lambda x: x['title']))
  237. @app.route("/get_vehicles")
  238. def get_vehicles():
  239. return jsonify(versions_fetcher.get_all_vehicles_sorted_uniq())
  240. @app.route("/get_defaults/<string:vehicle_name>/<string:remote_name>/<string:commit_reference>/<string:board_name>", methods = ['GET'])
  241. def get_deafults(vehicle_name, remote_name, commit_reference, board_name):
  242. # Heli is built on copter
  243. if vehicle_name == "Heli":
  244. vehicle_name = "Copter"
  245. commit_reference = base64.urlsafe_b64decode(commit_reference).decode()
  246. version_info = versions_fetcher.get_version_info(vehicle=vehicle_name, remote=remote_name, commit_ref=commit_reference)
  247. if version_info is None:
  248. return "Bad request. Commit reference %s is not allowed for builds for the %s for %s remote." % (commit_reference, vehicle_name, remote_name), 400
  249. artifacts_dir = version_info.ap_build_artifacts_url
  250. if artifacts_dir is None:
  251. return "Couldn't find artifacts for requested release/branch/commit on ardupilot server", 404
  252. url_to_features_txt = artifacts_dir + '/' + board_name + '/features.txt'
  253. response = requests.get(url_to_features_txt, timeout=30)
  254. if not response.status_code == 200:
  255. return ("Could not retrieve features.txt for given vehicle, version and board combination (Status Code: %d, url: %s)" % (response.status_code, url_to_features_txt), response.status_code)
  256. # split response by new line character to get a list of defines
  257. result = response.text.split('\n')
  258. # omit the last two elements as they are always blank
  259. return jsonify(result[:-2])
  260. if __name__ == '__main__':
  261. app.run()