policy.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  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. from .config import Config
  18. from .dbconn import Component, Override, OverrideType, PolicyQueueUpload, Priority, Section, Suite, get_mapped_component, get_mapped_component_name
  19. from .fstransactions import FilesystemTransaction
  20. from .regexes import re_file_changes, re_file_safe
  21. from .packagelist import PackageList
  22. import daklib.utils as utils
  23. import errno
  24. import os
  25. import shutil
  26. from typing import Optional
  27. class UploadCopy:
  28. """export a policy queue upload
  29. This class can be used in a with-statement::
  30. with UploadCopy(...) as copy:
  31. ...
  32. Doing so will provide a temporary copy of the upload in the directory
  33. given by the :attr:`directory` attribute. The copy will be removed
  34. on leaving the with-block.
  35. """
  36. def __init__(self, upload: PolicyQueueUpload, group: Optional[str] = None):
  37. """initializer
  38. :param upload: upload to handle
  39. """
  40. self.directory: Optional[str] = None
  41. self.upload = upload
  42. self.group = group
  43. def export(self, directory: str, mode: Optional[int] = None, symlink: bool = True, ignore_existing: bool = False) -> None:
  44. """export a copy of the upload
  45. :param directory: directory to export to
  46. :param mode: permissions to use for the copied files
  47. :param symlink: use symlinks instead of copying the files
  48. :param ignore_existing: ignore already existing files
  49. """
  50. with FilesystemTransaction() as fs:
  51. source = self.upload.source
  52. queue = self.upload.policy_queue
  53. if source is not None:
  54. for dsc_file in source.srcfiles:
  55. f = dsc_file.poolfile
  56. dst = os.path.join(directory, os.path.basename(f.filename))
  57. if not os.path.exists(dst) or not ignore_existing:
  58. fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
  59. for binary in self.upload.binaries:
  60. f = binary.poolfile
  61. dst = os.path.join(directory, os.path.basename(f.filename))
  62. if not os.path.exists(dst) or not ignore_existing:
  63. fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
  64. # copy byhand files
  65. for byhand in self.upload.byhand:
  66. src = os.path.join(queue.path, byhand.filename)
  67. dst = os.path.join(directory, byhand.filename)
  68. if os.path.exists(src) and (not os.path.exists(dst) or not ignore_existing):
  69. fs.copy(src, dst, mode=mode, symlink=symlink)
  70. # copy .changes
  71. src = os.path.join(queue.path, self.upload.changes.changesname)
  72. dst = os.path.join(directory, self.upload.changes.changesname)
  73. if not os.path.exists(dst) or not ignore_existing:
  74. fs.copy(src, dst, mode=mode, symlink=symlink)
  75. def __enter__(self):
  76. assert self.directory is None
  77. mode = 0o0700
  78. symlink = True
  79. if self.group is not None:
  80. mode = 0o2750
  81. symlink = False
  82. cnf = Config()
  83. self.directory = utils.temp_dirname(parent=cnf.get('Dir::TempPath'),
  84. mode=mode,
  85. group=self.group)
  86. self.export(self.directory, symlink=symlink)
  87. return self
  88. def __exit__(self, *args):
  89. if self.directory is not None:
  90. shutil.rmtree(self.directory)
  91. self.directory = None
  92. return None
  93. class PolicyQueueUploadHandler:
  94. """process uploads to policy queues
  95. This class allows to accept or reject uploads and to get a list of missing
  96. overrides (for NEW processing).
  97. """
  98. def __init__(self, upload: PolicyQueueUpload, session):
  99. """initializer
  100. :param upload: upload to process
  101. :param session: database session
  102. """
  103. self.upload = upload
  104. self.session = session
  105. @property
  106. def _overridesuite(self) -> Suite:
  107. overridesuite = self.upload.target_suite
  108. if overridesuite.overridesuite is not None:
  109. overridesuite = self.session.query(Suite).filter_by(suite_name=overridesuite.overridesuite).one()
  110. return overridesuite
  111. def _source_override(self, component_name: str) -> Override:
  112. package = self.upload.source.source
  113. suite = self._overridesuite
  114. component = get_mapped_component(component_name, self.session)
  115. query = self.session.query(Override).filter_by(package=package, suite=suite) \
  116. .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
  117. .filter(Override.component == component)
  118. return query.first()
  119. def _binary_override(self, name: str, binarytype, component_name: str) -> Override:
  120. suite = self._overridesuite
  121. component = get_mapped_component(component_name, self.session)
  122. query = self.session.query(Override).filter_by(package=name, suite=suite) \
  123. .join(OverrideType).filter(OverrideType.overridetype == binarytype) \
  124. .filter(Override.component == component)
  125. return query.first()
  126. @property
  127. def _changes_prefix(self) -> str:
  128. changesname = self.upload.changes.changesname
  129. assert changesname.endswith('.changes')
  130. assert re_file_changes.match(changesname)
  131. return changesname[0:-8]
  132. def accept(self) -> None:
  133. """mark upload as accepted"""
  134. assert len(self.missing_overrides()) == 0
  135. fn1 = 'ACCEPT.{0}'.format(self._changes_prefix)
  136. fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
  137. try:
  138. fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
  139. with os.fdopen(fh, 'wt') as f:
  140. f.write('OK\n')
  141. except OSError as e:
  142. if e.errno == errno.EEXIST:
  143. pass
  144. else:
  145. raise
  146. def reject(self, reason: str) -> None:
  147. """mark upload as rejected
  148. :param reason: reason for the rejection
  149. """
  150. cnf = Config()
  151. fn1 = 'REJECT.{0}'.format(self._changes_prefix)
  152. assert re_file_safe.match(fn1)
  153. fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
  154. try:
  155. fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
  156. with os.fdopen(fh, 'wt') as f:
  157. f.write('NOTOK\n')
  158. f.write('From: {0} <{1}>\n\n'.format(utils.whoami(), cnf['Dinstall::MyAdminAddress']))
  159. f.write(reason)
  160. except OSError as e:
  161. if e.errno == errno.EEXIST:
  162. pass
  163. else:
  164. raise
  165. def get_action(self) -> Optional[str]:
  166. """get current action
  167. :return: string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT'
  168. """
  169. changes_prefix = self._changes_prefix
  170. for action in ('ACCEPT', 'ACCEPTED', 'REJECT'):
  171. fn1 = '{0}.{1}'.format(action, changes_prefix)
  172. fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
  173. if os.path.exists(fn):
  174. return action
  175. return None
  176. def missing_overrides(self, hints: Optional[list[dict]] = None) -> list[dict]:
  177. """get missing override entries for the upload
  178. :param hints: suggested hints for new overrides in the same format as
  179. the return value
  180. :return: list of dicts with the following keys:
  181. - package: package name
  182. - priority: default priority (from upload)
  183. - section: default section (from upload)
  184. - component: default component (from upload)
  185. - type: type of required override ('dsc', 'deb' or 'udeb')
  186. All values are strings.
  187. """
  188. # TODO: use Package-List field
  189. missing = []
  190. components = set()
  191. source = self.upload.source
  192. if hints is None:
  193. hints = []
  194. hints_map = dict([((o['type'], o['package']), o) for o in hints])
  195. def check_override(name, type, priority, section, included):
  196. component = 'main'
  197. if section.find('/') != -1:
  198. component = section.split('/', 1)[0]
  199. override = self._binary_override(name, type, component)
  200. if override is None and not any(o['package'] == name and o['type'] == type for o in missing):
  201. hint = hints_map.get((type, name))
  202. if hint is not None:
  203. missing.append(hint)
  204. component = hint['component']
  205. else:
  206. missing.append(
  207. dict(
  208. package=name,
  209. priority=priority,
  210. section=section,
  211. component=component,
  212. type=type,
  213. included=included
  214. ))
  215. components.add(component)
  216. for binary in self.upload.binaries:
  217. binary_proxy = binary.proxy
  218. priority = binary_proxy.get('Priority', 'optional')
  219. section = binary_proxy['Section']
  220. check_override(binary.package, binary.binarytype, priority, section, included=True)
  221. if source is not None:
  222. source_proxy = source.proxy
  223. package_list = PackageList(source_proxy)
  224. if not package_list.fallback:
  225. packages = package_list.packages_for_suite(self.upload.target_suite)
  226. for p in packages:
  227. check_override(p.name, p.type, p.priority, p.section, included=False)
  228. # see daklib.archive.source_component_from_package_list
  229. # which we cannot use here as we might not have a Package-List
  230. # field for old packages
  231. mapped_components = [get_mapped_component_name(c) for c in components]
  232. query = self.session.query(Component).order_by(Component.ordering) \
  233. .filter(Component.component_name.in_(mapped_components))
  234. source_component = query.first().component_name
  235. override = self._source_override(source_component)
  236. if override is None:
  237. hint = hints_map.get(('dsc', source.source))
  238. if hint is not None:
  239. missing.append(hint)
  240. else:
  241. section = 'misc'
  242. if source_component != 'main':
  243. section = "{0}/{1}".format(source_component, section)
  244. missing.append(
  245. dict(
  246. package=source.source,
  247. priority='optional',
  248. section=section,
  249. component=source_component,
  250. type='dsc',
  251. included=True,
  252. ))
  253. return missing
  254. def add_overrides(self, new_overrides, suite: Suite) -> None:
  255. if suite.overridesuite is not None:
  256. suite = self.session.query(Suite).filter_by(suite_name=suite.overridesuite).one()
  257. for override in new_overrides:
  258. package = override['package']
  259. priority = self.session.query(Priority).filter_by(priority=override['priority']).first()
  260. section = self.session.query(Section).filter_by(section=override['section']).first()
  261. component = get_mapped_component(override['component'], self.session)
  262. overridetype = self.session.query(OverrideType).filter_by(overridetype=override['type']).one()
  263. if priority is None:
  264. raise Exception('Invalid priority {0} for package {1}'.format(priority, package))
  265. if section is None:
  266. raise Exception('Invalid section {0} for package {1}'.format(section, package))
  267. if component is None:
  268. raise Exception('Invalid component {0} for package {1}'.format(component, package))
  269. o = Override(package=package, suite=suite, component=component, priority=priority, section=section, overridetype=overridetype)
  270. self.session.add(o)
  271. self.session.commit()