versions_fetcher.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import logging
  2. import os
  3. import ap_git
  4. import json
  5. import jsonschema
  6. from pathlib import Path
  7. from threading import Lock
  8. from utils import TaskRunner
  9. class VersionInfo:
  10. """
  11. Class to wrap version info properties.
  12. """
  13. def __init__(self,
  14. remote: str,
  15. commit_ref: str,
  16. release_type: str,
  17. version_number: str,
  18. ap_build_artifacts_url) -> None:
  19. self.remote = remote
  20. self.commit_ref = commit_ref
  21. self.release_type = release_type
  22. self.version_number = version_number
  23. self.ap_build_artifacts_url = ap_build_artifacts_url
  24. class RemoteInfo:
  25. """
  26. Class to wrap remote info properties.
  27. """
  28. def __init__(self,
  29. name: str,
  30. url: str) -> None:
  31. self.name = name
  32. self.url = url
  33. class VersionsFetcher:
  34. """
  35. Class to fetch the version-to-build metadata from remotes.json
  36. and provide methods to view the same
  37. """
  38. __singleton = None
  39. def __init__(self, remotes_json_path: str,
  40. ap_repo: ap_git.GitRepo):
  41. """
  42. Initializes the VersionsFetcher instance
  43. with a given remotes.json path.
  44. Parameters:
  45. remotes_json_path (str): Path to the remotes.json file.
  46. ap_repo (GitRepo): ArduPilot local git repository. This local
  47. repository is shared between the VersionsFetcher
  48. and the APSourceMetadataFetcher.
  49. Raises:
  50. RuntimeError: If an instance of this class already exists,
  51. enforcing a singleton pattern.
  52. """
  53. # Enforce singleton pattern by raising an error if
  54. # an instance already exists.
  55. if VersionsFetcher.__singleton:
  56. raise RuntimeError("VersionsFetcher must be a singleton.")
  57. self.logger = logging.getLogger(__name__)
  58. self.__remotes_json_path = remotes_json_path
  59. self.__ensure_remotes_json()
  60. self.__access_lock_versions_metadata = Lock()
  61. self.__versions_metadata = []
  62. tasks = (
  63. (self.fetch_ap_releases, 1200),
  64. (self.fetch_whitelisted_tags, 1200),
  65. )
  66. self.__task__runner = TaskRunner(tasks=tasks)
  67. self.repo = ap_repo
  68. VersionsFetcher.__singleton = self
  69. def start(self) -> None:
  70. """
  71. Start auto-fetch jobs.
  72. """
  73. self.logger.info(
  74. "Starting VersionsFetcher background auto-fetch jobs."
  75. )
  76. self.__task__runner.start()
  77. def get_all_remotes_info(self) -> list[RemoteInfo]:
  78. """
  79. Return the list of RemoteInfo objects constructed from the
  80. information in the remotes.json file
  81. Returns:
  82. list: RemoteInfo objects for all remotes mentioned in remotes.json
  83. """
  84. return [
  85. RemoteInfo(
  86. name=remote.get('name', None),
  87. url=remote.get('url', None)
  88. )
  89. for remote in self.__get_versions_metadata()
  90. ]
  91. def get_remote_info(self, remote_name: str) -> RemoteInfo:
  92. """
  93. Return the RemoteInfo for the given remote name, None otherwise.
  94. Returns:
  95. RemoteInfo: The remote information object.
  96. """
  97. return next(
  98. (
  99. remote for remote in self.get_all_remotes_info()
  100. if remote.name == remote_name
  101. ),
  102. None
  103. )
  104. def get_versions_for_vehicle(self, vehicle_name: str) -> list[VersionInfo]:
  105. """
  106. Return the list of dictionaries containing the info about the
  107. versions listed to be built for a particular vehicle.
  108. Parameters:
  109. vehicle_name (str): the vehicle to fetch versions list for
  110. Returns:
  111. list: VersionInfo objects for all versions allowed to be
  112. built for the said vehicle.
  113. """
  114. if vehicle_name is None:
  115. raise ValueError("Vehicle is a required parameter.")
  116. versions_list = []
  117. for remote in self.__get_versions_metadata():
  118. for vehicle in remote['vehicles']:
  119. if vehicle['name'] != vehicle_name:
  120. continue
  121. for release in vehicle['releases']:
  122. versions_list.append(VersionInfo(
  123. remote=remote.get('name', None),
  124. commit_ref=release.get('commit_reference', None),
  125. release_type=release.get('release_type', None),
  126. version_number=release.get('version_number', None),
  127. ap_build_artifacts_url=release.get(
  128. 'ap_build_artifacts_url',
  129. None
  130. )
  131. ))
  132. return versions_list
  133. def get_all_vehicles_sorted_uniq(self) -> list[str]:
  134. """
  135. Return a sorted list of all vehicles listed in remotes.json structure
  136. Returns:
  137. list: Vehicles listed in remotes.json
  138. """
  139. vehicles_set = set()
  140. for remote in self.__get_versions_metadata():
  141. for vehicle in remote['vehicles']:
  142. vehicles_set.add(vehicle['name'])
  143. return sorted(list(vehicles_set))
  144. def is_version_listed(self, vehicle: str, remote: str,
  145. commit_ref: str) -> bool:
  146. """
  147. Check if a version with given properties mentioned in remotes.json
  148. Parameters:
  149. vehicle (str): vehicle for which version is listed
  150. remote (str): remote under which the version is listed
  151. commit_ref(str): commit reference for the version
  152. Returns:
  153. bool: True if the said version is mentioned in remotes.json,
  154. False otherwise
  155. """
  156. if vehicle is None:
  157. raise ValueError("Vehicle is a required parameter.")
  158. if remote is None:
  159. raise ValueError("Remote is a required parameter.")
  160. if commit_ref is None:
  161. raise ValueError("Commit reference is a required parameter.")
  162. return (remote, commit_ref) in [
  163. (version_info.remote, version_info.commit_ref)
  164. for version_info in
  165. self.get_versions_for_vehicle(vehicle_name=vehicle)
  166. ]
  167. def get_version_info(self, vehicle: str, remote: str,
  168. commit_ref: str) -> VersionInfo:
  169. """
  170. Find first version matching the given properties in remotes.json
  171. Parameters:
  172. vehicle (str): vehicle for which version is listed
  173. remote (str): remote under which the version is listed
  174. commit_ref(str): commit reference for the version
  175. Returns:
  176. VersionInfo: Object for the version matching the properties,
  177. None if not found
  178. """
  179. return next(
  180. (
  181. version
  182. for version in self.get_versions_for_vehicle(
  183. vehicle_name=vehicle
  184. )
  185. if version.remote == remote and
  186. version.commit_ref == commit_ref
  187. ),
  188. None
  189. )
  190. def reload_remotes_json(self) -> None:
  191. """
  192. Read remotes.json, validate its structure against the schema
  193. and cache it in memory
  194. """
  195. # load file containing vehicles listed to be built for each
  196. # remote along with the branches/tags/commits on which the
  197. # firmware can be built
  198. remotes_json_schema_path = os.path.join(
  199. os.path.dirname(__file__),
  200. 'remotes.schema.json'
  201. )
  202. with open(self.__remotes_json_path, 'r') as f, \
  203. open(remotes_json_schema_path, 'r') as s:
  204. f_content = f.read()
  205. # Early return if file is empty
  206. if not f_content:
  207. return
  208. versions_metadata = json.loads(f_content)
  209. schema = json.loads(s.read())
  210. # validate schema
  211. jsonschema.validate(instance=versions_metadata, schema=schema)
  212. self.__set_versions_metadata(versions_metadata=versions_metadata)
  213. # update git repo with latest remotes list
  214. self.__sync_remotes_with_ap_repo()
  215. def __ensure_remotes_json(self) -> None:
  216. """
  217. Ensures remotes.json exists and is a valid JSON file.
  218. """
  219. p = Path(self.__remotes_json_path)
  220. if not p.exists():
  221. # Ensure parent directory exists
  222. Path.mkdir(p.parent, parents=True, exist_ok=True)
  223. # write empty json list
  224. with open(p, 'w') as f:
  225. f.write('[]')
  226. def __set_versions_metadata(self, versions_metadata: list) -> None:
  227. """
  228. Set versions metadata property with the one passed as parameter
  229. This requires to acquire the access lock to avoid overwriting the
  230. object while it is being read
  231. """
  232. if versions_metadata is None:
  233. raise ValueError("versions_metadata is a required parameter. "
  234. "Cannot be None.")
  235. with self.__access_lock_versions_metadata:
  236. self.__versions_metadata = versions_metadata
  237. def __get_versions_metadata(self) -> list:
  238. """
  239. Read versions metadata property
  240. This requires to acquire the access lock to avoid reading the list
  241. while it is being modified
  242. Returns:
  243. list: the versions metadata list
  244. """
  245. with self.__access_lock_versions_metadata:
  246. return self.__versions_metadata
  247. def __sync_remotes_with_ap_repo(self):
  248. """
  249. Update the remotes in ArduPilot local repository with the latest
  250. remotes list.
  251. """
  252. remotes = tuple(
  253. (remote.name, remote.url)
  254. for remote in self.get_all_remotes_info()
  255. )
  256. self.repo.remote_add_bulk(remotes=remotes, force=True)
  257. def fetch_ap_releases(self) -> None:
  258. """
  259. Execute the fetch_releases.py script to update remotes.json
  260. with Ardupilot's official releases
  261. """
  262. from scripts import fetch_releases
  263. fetch_releases.run(
  264. base_dir=os.path.join(
  265. os.path.dirname(self.__remotes_json_path),
  266. '..',
  267. ),
  268. remote_name="ardupilot",
  269. )
  270. self.reload_remotes_json()
  271. return
  272. def fetch_whitelisted_tags(self) -> None:
  273. """
  274. Execute the fetch_whitelisted_tags.py script to update
  275. remotes.json with tags from whitelisted repos
  276. """
  277. from scripts import fetch_whitelisted_tags
  278. fetch_whitelisted_tags.run(
  279. base_dir=os.path.join(
  280. os.path.dirname(self.__remotes_json_path),
  281. '..',
  282. )
  283. )
  284. self.reload_remotes_json()
  285. return
  286. @staticmethod
  287. def get_singleton():
  288. return VersionsFetcher.__singleton