policy.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. # Copyright (C) 2012, Ansgar Burchardt <ansgar@debian.org>
  2. #
  3. # This program is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License as published by
  5. # the Free Software Foundation; either version 2 of the License, or
  6. # (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License along
  14. # with this program; if not, write to the Free Software Foundation, Inc.,
  15. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  16. """module to process policy queue uploads"""
  17. import errno
  18. import os
  19. import shutil
  20. from typing import Optional
  21. import daklib.utils as utils
  22. from .config import Config
  23. from .dbconn import (
  24. Component,
  25. Override,
  26. OverrideType,
  27. PolicyQueueUpload,
  28. Priority,
  29. Section,
  30. Suite,
  31. get_mapped_component,
  32. get_mapped_component_name,
  33. )
  34. from .fstransactions import FilesystemTransaction
  35. from .packagelist import PackageList
  36. from .regexes import re_file_changes, re_file_safe
  37. class UploadCopy:
  38. """export a policy queue upload
  39. This class can be used in a with-statement::
  40. with UploadCopy(...) as copy:
  41. ...
  42. Doing so will provide a temporary copy of the upload in the directory
  43. given by the :attr:`directory` attribute. The copy will be removed
  44. on leaving the with-block.
  45. """
  46. def __init__(self, upload: PolicyQueueUpload, group: Optional[str] = None):
  47. """initializer
  48. :param upload: upload to handle
  49. """
  50. self.directory: Optional[str] = None
  51. self.upload = upload
  52. self.group = group
  53. def export(
  54. self,
  55. directory: str,
  56. mode: Optional[int] = None,
  57. symlink: bool = True,
  58. ignore_existing: bool = False,
  59. ) -> None:
  60. """export a copy of the upload
  61. :param directory: directory to export to
  62. :param mode: permissions to use for the copied files
  63. :param symlink: use symlinks instead of copying the files
  64. :param ignore_existing: ignore already existing files
  65. """
  66. with FilesystemTransaction() as fs:
  67. source = self.upload.source
  68. queue = self.upload.policy_queue
  69. if source is not None:
  70. for dsc_file in source.srcfiles:
  71. f = dsc_file.poolfile
  72. dst = os.path.join(directory, os.path.basename(f.filename))
  73. if not os.path.exists(dst) or not ignore_existing:
  74. fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
  75. for binary in self.upload.binaries:
  76. f = binary.poolfile
  77. dst = os.path.join(directory, os.path.basename(f.filename))
  78. if not os.path.exists(dst) or not ignore_existing:
  79. fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
  80. # copy byhand files
  81. for byhand in self.upload.byhand:
  82. src = os.path.join(queue.path, byhand.filename)
  83. dst = os.path.join(directory, byhand.filename)
  84. if os.path.exists(src) and (
  85. not os.path.exists(dst) or not ignore_existing
  86. ):
  87. fs.copy(src, dst, mode=mode, symlink=symlink)
  88. # copy .changes
  89. src = os.path.join(queue.path, self.upload.changes.changesname)
  90. dst = os.path.join(directory, self.upload.changes.changesname)
  91. if not os.path.exists(dst) or not ignore_existing:
  92. fs.copy(src, dst, mode=mode, symlink=symlink)
  93. def __enter__(self):
  94. assert self.directory is None
  95. mode = 0o0700
  96. symlink = True
  97. if self.group is not None:
  98. mode = 0o2750
  99. symlink = False
  100. cnf = Config()
  101. self.directory = utils.temp_dirname(
  102. parent=cnf.get("Dir::TempPath"), mode=mode, group=self.group
  103. )
  104. self.export(self.directory, symlink=symlink)
  105. return self
  106. def __exit__(self, *args):
  107. if self.directory is not None:
  108. shutil.rmtree(self.directory)
  109. self.directory = None
  110. return None
  111. class PolicyQueueUploadHandler:
  112. """process uploads to policy queues
  113. This class allows to accept or reject uploads and to get a list of missing
  114. overrides (for NEW processing).
  115. """
  116. def __init__(self, upload: PolicyQueueUpload, session):
  117. """initializer
  118. :param upload: upload to process
  119. :param session: database session
  120. """
  121. self.upload = upload
  122. self.session = session
  123. @property
  124. def _overridesuite(self) -> Suite:
  125. overridesuite = self.upload.target_suite
  126. if overridesuite.overridesuite is not None:
  127. overridesuite = (
  128. self.session.query(Suite)
  129. .filter_by(suite_name=overridesuite.overridesuite)
  130. .one()
  131. )
  132. return overridesuite
  133. def _source_override(self, component_name: str) -> Override:
  134. package = self.upload.source.source
  135. suite = self._overridesuite
  136. component = get_mapped_component(component_name, self.session)
  137. query = (
  138. self.session.query(Override)
  139. .filter_by(package=package, suite=suite)
  140. .join(OverrideType)
  141. .filter(OverrideType.overridetype == "dsc")
  142. .filter(Override.component == component)
  143. )
  144. return query.first()
  145. def _binary_override(self, name: str, binarytype, component_name: str) -> Override:
  146. suite = self._overridesuite
  147. component = get_mapped_component(component_name, self.session)
  148. query = (
  149. self.session.query(Override)
  150. .filter_by(package=name, suite=suite)
  151. .join(OverrideType)
  152. .filter(OverrideType.overridetype == binarytype)
  153. .filter(Override.component == component)
  154. )
  155. return query.first()
  156. @property
  157. def _changes_prefix(self) -> str:
  158. changesname = self.upload.changes.changesname
  159. assert changesname.endswith(".changes")
  160. assert re_file_changes.match(changesname)
  161. return changesname[0:-8]
  162. def accept(self) -> None:
  163. """mark upload as accepted"""
  164. assert len(self.missing_overrides()) == 0
  165. fn1 = "ACCEPT.{0}".format(self._changes_prefix)
  166. fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
  167. try:
  168. fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
  169. with os.fdopen(fh, "wt") as f:
  170. f.write("OK\n")
  171. except OSError as e:
  172. if e.errno == errno.EEXIST:
  173. pass
  174. else:
  175. raise
  176. def reject(self, reason: str) -> None:
  177. """mark upload as rejected
  178. :param reason: reason for the rejection
  179. """
  180. cnf = Config()
  181. fn1 = "REJECT.{0}".format(self._changes_prefix)
  182. assert re_file_safe.match(fn1)
  183. fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
  184. try:
  185. fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
  186. with os.fdopen(fh, "wt") as f:
  187. f.write("NOTOK\n")
  188. f.write(
  189. "From: {0} <{1}>\n\n".format(
  190. utils.whoami(), cnf["Dinstall::MyAdminAddress"]
  191. )
  192. )
  193. f.write(reason)
  194. except OSError as e:
  195. if e.errno == errno.EEXIST:
  196. pass
  197. else:
  198. raise
  199. def get_action(self) -> Optional[str]:
  200. """get current action
  201. :return: string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT'
  202. """
  203. changes_prefix = self._changes_prefix
  204. for action in ("ACCEPT", "ACCEPTED", "REJECT"):
  205. fn1 = "{0}.{1}".format(action, changes_prefix)
  206. fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
  207. if os.path.exists(fn):
  208. return action
  209. return None
  210. def missing_overrides(self, hints: Optional[list[dict]] = None) -> list[dict]:
  211. """get missing override entries for the upload
  212. :param hints: suggested hints for new overrides in the same format as
  213. the return value
  214. :return: list of dicts with the following keys:
  215. - package: package name
  216. - priority: default priority (from upload)
  217. - section: default section (from upload)
  218. - component: default component (from upload)
  219. - type: type of required override ('dsc', 'deb' or 'udeb')
  220. All values are strings.
  221. """
  222. # TODO: use Package-List field
  223. missing = []
  224. components = set()
  225. source = self.upload.source
  226. if hints is None:
  227. hints = []
  228. hints_map = dict([((o["type"], o["package"]), o) for o in hints])
  229. def check_override(name, type, priority, section, included):
  230. component = "main"
  231. if section.find("/") != -1:
  232. component = section.split("/", 1)[0]
  233. override = self._binary_override(name, type, component)
  234. if override is None and not any(
  235. o["package"] == name and o["type"] == type for o in missing
  236. ):
  237. hint = hints_map.get((type, name))
  238. if hint is not None:
  239. missing.append(hint)
  240. component = hint["component"]
  241. else:
  242. missing.append(
  243. dict(
  244. package=name,
  245. priority=priority,
  246. section=section,
  247. component=component,
  248. type=type,
  249. included=included,
  250. )
  251. )
  252. components.add(component)
  253. for binary in self.upload.binaries:
  254. binary_proxy = binary.proxy
  255. priority = binary_proxy.get("Priority", "optional")
  256. section = binary_proxy["Section"]
  257. check_override(
  258. binary.package, binary.binarytype, priority, section, included=True
  259. )
  260. if source is not None:
  261. source_proxy = source.proxy
  262. package_list = PackageList(source_proxy)
  263. if not package_list.fallback:
  264. packages = package_list.packages_for_suite(self.upload.target_suite)
  265. for p in packages:
  266. check_override(
  267. p.name, p.type, p.priority, p.section, included=False
  268. )
  269. # see daklib.archive.source_component_from_package_list
  270. # which we cannot use here as we might not have a Package-List
  271. # field for old packages
  272. mapped_components = [get_mapped_component_name(c) for c in components]
  273. query = (
  274. self.session.query(Component)
  275. .order_by(Component.ordering)
  276. .filter(Component.component_name.in_(mapped_components))
  277. )
  278. source_component = query.first().component_name
  279. override = self._source_override(source_component)
  280. if override is None:
  281. hint = hints_map.get(("dsc", source.source))
  282. if hint is not None:
  283. missing.append(hint)
  284. else:
  285. section = "misc"
  286. if source_component != "main":
  287. section = "{0}/{1}".format(source_component, section)
  288. missing.append(
  289. dict(
  290. package=source.source,
  291. priority="optional",
  292. section=section,
  293. component=source_component,
  294. type="dsc",
  295. included=True,
  296. )
  297. )
  298. return missing
  299. def add_overrides(self, new_overrides, suite: Suite) -> None:
  300. if suite.overridesuite is not None:
  301. suite = (
  302. self.session.query(Suite)
  303. .filter_by(suite_name=suite.overridesuite)
  304. .one()
  305. )
  306. for override in new_overrides:
  307. package = override["package"]
  308. priority = (
  309. self.session.query(Priority)
  310. .filter_by(priority=override["priority"])
  311. .first()
  312. )
  313. section = (
  314. self.session.query(Section)
  315. .filter_by(section=override["section"])
  316. .first()
  317. )
  318. component = get_mapped_component(override["component"], self.session)
  319. overridetype = (
  320. self.session.query(OverrideType)
  321. .filter_by(overridetype=override["type"])
  322. .one()
  323. )
  324. if priority is None:
  325. raise Exception(
  326. "Invalid priority {0} for package {1}".format(priority, package)
  327. )
  328. if section is None:
  329. raise Exception(
  330. "Invalid section {0} for package {1}".format(section, package)
  331. )
  332. if component is None:
  333. raise Exception(
  334. "Invalid component {0} for package {1}".format(component, package)
  335. )
  336. o = Override(
  337. package=package,
  338. suite=suite,
  339. component=component,
  340. priority=priority,
  341. section=section,
  342. overridetype=overridetype,
  343. )
  344. self.session.add(o)
  345. self.session.commit()