ap_src_meta_fetcher.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import redis
  2. import dill
  3. import fnmatch
  4. import time
  5. import logging
  6. import ap_git
  7. import os
  8. class APSourceMetadataFetcher:
  9. """
  10. Class to fetch metadata like available boards, features etc.
  11. from the AP source code
  12. """
  13. __singleton = None
  14. def __init__(self, ap_repo: ap_git.GitRepo,
  15. caching_enabled: bool = False,
  16. redis_host: str = 'localhost',
  17. redis_port: str = '6379') -> None:
  18. """
  19. Initializes the APSourceMetadataFetcher instance
  20. with a given repository path.
  21. Parameters:
  22. ap_repo (GitRepo): ArduPilot local git repository containing
  23. the metadata generation scripts.
  24. caching_enabled (bool): Enable caching metadata for each commit to
  25. avoid checking out git repo each time.
  26. redis_host (str): Hostname of the Redis instance to use for caching
  27. metadata.
  28. redis_port (int): Port of the Redis instance to use for caching
  29. metadata
  30. Raises:
  31. RuntimeError: If an instance of this class already exists,
  32. enforcing a singleton pattern.
  33. """
  34. # Enforce singleton pattern by raising an error if
  35. # an instance already exists.
  36. if APSourceMetadataFetcher.__singleton:
  37. raise RuntimeError(
  38. "APSourceMetadataFetcher must be a singleton."
  39. )
  40. self.logger = logging.getLogger(__name__)
  41. self.repo = ap_repo
  42. self.caching_enabled = caching_enabled
  43. if self.caching_enabled:
  44. self.__redis_client = redis.Redis(
  45. host=redis_host,
  46. port=redis_port,
  47. decode_responses=False,
  48. )
  49. self.logger.info(
  50. f"Redis connection established with {redis_host}:{redis_port}"
  51. )
  52. self.__boards_key_prefix = "boards-"
  53. self.__build_options_key_prefix = "bopts-"
  54. APSourceMetadataFetcher.__singleton = self
  55. def __boards_key(self, commit_id: str) -> str:
  56. """
  57. Generate the Redis key that stores the boards list for a given commit.
  58. Parameters:
  59. commit_id (str): The git sha for the commit.
  60. Returns:
  61. str: The Redis key containing the cached board list.
  62. """
  63. return self.__boards_key_prefix + f"{commit_id}"
  64. def __build_options_key(self, commit_id: str) -> str:
  65. """
  66. Generate the Redis key that stores the build options list for a given
  67. commit.
  68. Parameters:
  69. commit_id (str): The git sha for the commit.
  70. Returns:
  71. str: The Redis key containing the cached build options list.
  72. """
  73. return self.__build_options_key_prefix + f"{commit_id}"
  74. def __cache_boards_at_commit(self,
  75. boards: list,
  76. commit_id: str,
  77. ttl_sec: int = 86400) -> None:
  78. """
  79. Cache the given list of boards for a particular commit.
  80. Parameters:
  81. boards (list): The list of boards.
  82. commit_id (str): The git sha for the commit.
  83. ttl_sec (int): Time-to-live (TTL) in seconds after which the
  84. cached list expires.
  85. Raises:
  86. RuntimeError: If the method is called when caching is disabled.
  87. """
  88. if not self.caching_enabled:
  89. raise RuntimeError("Should not be called with caching disabled.")
  90. key = self.__boards_key(commit_id=commit_id)
  91. self.logger.debug(
  92. "Caching boards list "
  93. f"Redis key: {key}, "
  94. f"Boards: {boards}, "
  95. f"TTL: {ttl_sec} sec"
  96. )
  97. self.__redis_client.set(
  98. name=key,
  99. value=dill.dumps(boards),
  100. ex=ttl_sec
  101. )
  102. def __cache_build_options_at_commit(self,
  103. build_options: list,
  104. commit_id: str,
  105. ttl_sec: int = 86400) -> None:
  106. """
  107. Cache the given list of build options for a particular commit.
  108. Parameters:
  109. build_options (list): The list of build options.
  110. commit_id (str): The git sha for the commit.
  111. ttl_sec (int): Time-to-live (TTL) in seconds after which the
  112. cached list expires.
  113. Raises:
  114. RuntimeError: If the method is called when caching is disabled.
  115. """
  116. if not self.caching_enabled:
  117. raise RuntimeError("Should not be called with caching disabled.")
  118. key = self.__build_options_key(commit_id=commit_id)
  119. self.logger.debug(
  120. "Caching build options "
  121. f"Redis key: {key}, "
  122. f"Build Options: {build_options}, "
  123. f"TTL: {ttl_sec} sec"
  124. )
  125. self.__redis_client.set(
  126. name=key,
  127. value=dill.dumps(build_options),
  128. ex=ttl_sec
  129. )
  130. def __get_build_options_at_commit_from_cache(self,
  131. commit_id: str) -> list:
  132. """
  133. Retrieves a list of build options available at a specified commit
  134. from cache if exists, None otherwise.
  135. Parameters:
  136. commit_id (str): The commit id to get build options for.
  137. Returns:
  138. list: A list of build options available at the specified commit.
  139. Raises:
  140. RuntimeError: If the method is called when caching is disabled.
  141. """
  142. if not self.caching_enabled:
  143. raise RuntimeError("Should not be called with caching disabled.")
  144. key = self.__build_options_key(commit_id=commit_id)
  145. self.logger.debug(
  146. f"Getting cached build options for commit id {commit_id}, "
  147. f"Redis Key: {key}"
  148. )
  149. value = self.__redis_client.get(key)
  150. self.logger.debug(f"Got value {value} at key {key}")
  151. return dill.loads(value) if value else None
  152. def __get_boards_at_commit_from_cache(self, commit_id: str) -> list:
  153. """
  154. Returns the list of boards for a given commit from cache if exists,
  155. None otherwise.
  156. Parameters:
  157. commit_id (str): The commit id to get boards list for.
  158. Returns:
  159. list: A list of boards available at the specified commit.
  160. Raises:
  161. RuntimeError: If the method is called when caching is disabled.
  162. """
  163. if not self.caching_enabled:
  164. raise RuntimeError("Should not be called with caching disabled.")
  165. key = self.__boards_key(commit_id=commit_id)
  166. self.logger.debug(
  167. f"Getting cached boards list for commit id {commit_id}, "
  168. f"Redis Key: {key}"
  169. )
  170. value = self.__redis_client.get(key)
  171. self.logger.debug(f"Got value {value} at key {key}")
  172. return dill.loads(value) if value else None
  173. def __get_boards_at_commit_from_repo(self, remote: str,
  174. commit_ref: str) -> list:
  175. """
  176. Returns the list of boards for a given commit from the git repo.
  177. Parameters:
  178. remote (str): The name of the remote repository.
  179. commit_ref (str): The commit reference to check out.
  180. Returns:
  181. list: A list of boards available at the specified commit.
  182. """
  183. with self.repo.get_checkout_lock():
  184. self.repo.checkout_remote_commit_ref(
  185. remote=remote,
  186. commit_ref=commit_ref,
  187. force=True,
  188. hard_reset=True,
  189. clean_working_tree=True
  190. )
  191. import importlib.util
  192. spec = importlib.util.spec_from_file_location(
  193. name="board_list.py",
  194. location=os.path.join(
  195. self.repo.get_local_path(),
  196. 'Tools', 'scripts',
  197. 'board_list.py')
  198. )
  199. mod = importlib.util.module_from_spec(spec)
  200. spec.loader.exec_module(mod)
  201. all_boards = mod.AUTOBUILD_BOARDS
  202. exclude_patterns = ['fmuv*', 'SITL*']
  203. boards = []
  204. for b in all_boards:
  205. excluded = False
  206. for p in exclude_patterns:
  207. if fnmatch.fnmatch(b.lower(), p.lower()):
  208. excluded = True
  209. break
  210. if not excluded:
  211. boards.append(b)
  212. boards.sort()
  213. return boards
  214. def __get_build_options_at_commit_from_repo(self, remote: str,
  215. commit_ref: str) -> tuple:
  216. """
  217. Returns the list of build options for a given commit from the git repo.
  218. Parameters:
  219. remote (str): The name of the remote repository.
  220. commit_ref (str): The commit reference to check out.
  221. Returns:
  222. list: A list of build options available at the specified commit.
  223. """
  224. with self.repo.get_checkout_lock():
  225. self.repo.checkout_remote_commit_ref(
  226. remote=remote,
  227. commit_ref=commit_ref,
  228. force=True,
  229. hard_reset=True,
  230. clean_working_tree=True
  231. )
  232. import importlib.util
  233. spec = importlib.util.spec_from_file_location(
  234. name="build_options.py",
  235. location=os.path.join(
  236. self.repo.get_local_path(),
  237. 'Tools',
  238. 'scripts',
  239. 'build_options.py'
  240. )
  241. )
  242. mod = importlib.util.module_from_spec(spec)
  243. spec.loader.exec_module(mod)
  244. build_options = mod.BUILD_OPTIONS
  245. return build_options
  246. def get_boards_at_commit(self, remote: str,
  247. commit_ref: str) -> list:
  248. """
  249. Retrieves a list of boards available for building at a
  250. specified commit and returns the list.
  251. If caching is enabled, this would first look in the cache for
  252. the list. In case of a cache miss, it would retrive the list
  253. by checkout out the git repo and running `board_list.py` and
  254. cache it.
  255. Parameters:
  256. remote (str): The name of the remote repository.
  257. commit_ref (str): The commit reference to check out.
  258. Returns:
  259. list: A list of boards available at the specified commit.
  260. """
  261. tstart = time.time()
  262. if not self.caching_enabled:
  263. boards = self.__get_boards_at_commit_from_repo(
  264. remote=remote,
  265. commit_ref=commit_ref,
  266. )
  267. self.logger.debug(
  268. f"Took {(time.time() - tstart)} seconds to get boards"
  269. )
  270. return boards
  271. commid_id = self.repo.commit_id_for_remote_ref(
  272. remote=remote,
  273. commit_ref=commit_ref,
  274. )
  275. self.logger.debug(f"Fetching boards for commit {commid_id}.")
  276. cached_boards = self.__get_boards_at_commit_from_cache(
  277. commit_id=commid_id
  278. )
  279. if cached_boards:
  280. boards = cached_boards
  281. else:
  282. self.logger.debug(
  283. "Cache miss. Fetching boards from repo for "
  284. f"commit {commid_id}."
  285. )
  286. boards = self.__get_boards_at_commit_from_repo(
  287. remote=remote,
  288. commit_ref=commid_id,
  289. )
  290. self.__cache_boards_at_commit(
  291. boards=boards,
  292. commit_id=commid_id,
  293. )
  294. self.logger.debug(
  295. f"Took {(time.time() - tstart)} seconds to get boards"
  296. )
  297. return boards
  298. def get_build_options_at_commit(self, remote: str,
  299. commit_ref: str) -> list:
  300. """
  301. Retrieves a list of build options available at a specified commit.
  302. If caching is enabled, this would first look in the cache for
  303. the list. In case of a cache miss, it would retrive the list
  304. by checkout out the git repo and running `build_options.py` and
  305. cache it.
  306. Parameters:
  307. remote (str): The name of the remote repository.
  308. commit_ref (str): The commit reference to check out.
  309. Returns:
  310. list: A list of build options available at the specified commit.
  311. """
  312. tstart = time.time()
  313. if not self.caching_enabled:
  314. build_options = self.__get_build_options_at_commit_from_repo(
  315. remote=remote,
  316. commit_ref=commit_ref,
  317. )
  318. self.logger.debug(
  319. f"Took {(time.time() - tstart)} seconds to get build options"
  320. )
  321. return build_options
  322. commid_id = self.repo.commit_id_for_remote_ref(
  323. remote=remote,
  324. commit_ref=commit_ref,
  325. )
  326. self.logger.debug(f"Fetching build options for commit {commid_id}.")
  327. cached_build_options = self.__get_build_options_at_commit_from_cache(
  328. commit_id=commid_id
  329. )
  330. if cached_build_options:
  331. build_options = cached_build_options
  332. else:
  333. self.logger.debug(
  334. "Cache miss. Fetching build options from repo for "
  335. f"commit {commid_id}."
  336. )
  337. build_options = self.__get_build_options_at_commit_from_repo(
  338. remote=remote,
  339. commit_ref=commid_id,
  340. )
  341. self.__cache_build_options_at_commit(
  342. build_options=build_options,
  343. commit_id=commid_id,
  344. )
  345. self.logger.debug(
  346. f"Took {(time.time() - tstart)} seconds to get build options"
  347. )
  348. return build_options
  349. @staticmethod
  350. def get_singleton():
  351. return APSourceMetadataFetcher.__singleton