Coverage for daklib/policy.py: 88%
195 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-03-14 12:19 +0000
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.
17"""module to process policy queue uploads"""
19import errno
20import os
21import shutil
22from typing import TYPE_CHECKING, NotRequired, Optional, TypedDict
24import daklib.utils as utils
26from .config import Config
27from .dbconn import (
28 Component,
29 Override,
30 OverrideType,
31 PolicyQueueUpload,
32 Priority,
33 Section,
34 Suite,
35 get_mapped_component,
36 get_mapped_component_name,
37)
38from .fstransactions import FilesystemTransaction
39from .packagelist import PackageList
40from .regexes import re_file_changes, re_file_safe
42if TYPE_CHECKING:
43 from collections.abc import Iterable
45 from sqlalchemy.orm import Session
48class UploadCopy:
49 """export a policy queue upload
51 This class can be used in a with-statement::
53 with UploadCopy(...) as copy:
54 ...
56 Doing so will provide a temporary copy of the upload in the directory
57 given by the :attr:`directory` attribute. The copy will be removed
58 on leaving the with-block.
59 """
61 def __init__(self, upload: PolicyQueueUpload, group: Optional[str] = None):
62 """initializer
64 :param upload: upload to handle
65 """
67 self._directory: Optional[str] = None
68 self.upload = upload
69 self.group = group
71 @property
72 def directory(self) -> str:
73 assert self._directory is not None
74 return self._directory
76 def export(
77 self,
78 directory: str,
79 mode: Optional[int] = None,
80 symlink: bool = True,
81 ignore_existing: bool = False,
82 ) -> None:
83 """export a copy of the upload
85 :param directory: directory to export to
86 :param mode: permissions to use for the copied files
87 :param symlink: use symlinks instead of copying the files
88 :param ignore_existing: ignore already existing files
89 """
90 with FilesystemTransaction() as fs:
91 source = self.upload.source
92 queue = self.upload.policy_queue
94 if source is not None:
95 for dsc_file in source.srcfiles:
96 f = dsc_file.poolfile
97 dst = os.path.join(directory, os.path.basename(f.filename))
98 if not os.path.exists(dst) or not ignore_existing: 98 ↛ 95line 98 didn't jump to line 95 because the condition on line 98 was always true
99 fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
101 for binary in self.upload.binaries:
102 f = binary.poolfile
103 dst = os.path.join(directory, os.path.basename(f.filename))
104 if not os.path.exists(dst) or not ignore_existing: 104 ↛ 101line 104 didn't jump to line 101 because the condition on line 104 was always true
105 fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
107 # copy byhand files
108 for byhand in self.upload.byhand: 108 ↛ 109line 108 didn't jump to line 109 because the loop on line 108 never started
109 src = os.path.join(queue.path, byhand.filename)
110 dst = os.path.join(directory, byhand.filename)
111 if os.path.exists(src) and (
112 not os.path.exists(dst) or not ignore_existing
113 ):
114 fs.copy(src, dst, mode=mode, symlink=symlink)
116 # copy .changes
117 src = os.path.join(queue.path, self.upload.changes.changesname)
118 dst = os.path.join(directory, self.upload.changes.changesname)
119 if not os.path.exists(dst) or not ignore_existing: 119 ↛ 90line 119 didn't jump to line 90
120 fs.copy(src, dst, mode=mode, symlink=symlink)
122 def __enter__(self):
123 assert self._directory is None
125 mode = 0o0700
126 symlink = True
127 if self.group is not None: 127 ↛ 128line 127 didn't jump to line 128 because the condition on line 127 was never true
128 mode = 0o2750
129 symlink = False
131 cnf = Config()
132 self._directory = utils.temp_dirname(
133 parent=cnf.get("Dir::TempPath"), mode=mode, group=self.group
134 )
135 self.export(self.directory, symlink=symlink)
136 return self
138 def __exit__(self, *args):
139 if self._directory is not None: 139 ↛ 142line 139 didn't jump to line 142 because the condition on line 139 was always true
140 shutil.rmtree(self._directory)
141 self._directory = None
142 return None
145class MissingOverride(TypedDict):
146 package: str
147 priority: str
148 section: str
149 component: str
150 type: str
151 included: NotRequired[bool]
152 valid: NotRequired[bool]
155class PolicyQueueUploadHandler:
156 """process uploads to policy queues
158 This class allows to accept or reject uploads and to get a list of missing
159 overrides (for NEW processing).
160 """
162 def __init__(self, upload: PolicyQueueUpload, session: "Session"):
163 """initializer
165 :param upload: upload to process
166 :param session: database session
167 """
168 self.upload = upload
169 self.session = session
171 @property
172 def _overridesuite(self) -> Suite:
173 overridesuite = self.upload.target_suite
174 if overridesuite.overridesuite is not None:
175 overridesuite = (
176 self.session.query(Suite)
177 .filter_by(suite_name=overridesuite.overridesuite)
178 .one()
179 )
180 return overridesuite
182 def _source_override(self, component_name: str) -> Override | None:
183 assert self.upload.source is not None
184 package = self.upload.source.source
185 suite = self._overridesuite
186 component = get_mapped_component(component_name, self.session)
187 query = (
188 self.session.query(Override)
189 .filter_by(package=package, suite=suite)
190 .join(OverrideType)
191 .filter(OverrideType.overridetype == "dsc")
192 .filter(Override.component == component)
193 )
194 return query.first()
196 def _binary_override(
197 self, name: str, binarytype, component_name: str
198 ) -> Override | None:
199 suite = self._overridesuite
200 component = get_mapped_component(component_name, self.session)
201 query = (
202 self.session.query(Override)
203 .filter_by(package=name, suite=suite)
204 .join(OverrideType)
205 .filter(OverrideType.overridetype == binarytype)
206 .filter(Override.component == component)
207 )
208 return query.first()
210 @property
211 def _changes_prefix(self) -> str:
212 changesname = self.upload.changes.changesname
213 assert changesname.endswith(".changes")
214 assert re_file_changes.match(changesname)
215 return changesname[0:-8]
217 def accept(self) -> None:
218 """mark upload as accepted"""
219 assert len(self.missing_overrides()) == 0
221 fn1 = "ACCEPT.{0}".format(self._changes_prefix)
222 fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
223 try:
224 fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
225 with os.fdopen(fh, "wt") as f:
226 f.write("OK\n")
227 except OSError as e:
228 if e.errno == errno.EEXIST:
229 pass
230 else:
231 raise
233 def reject(self, reason: str) -> None:
234 """mark upload as rejected
236 :param reason: reason for the rejection
237 """
238 cnf = Config()
240 fn1 = "REJECT.{0}".format(self._changes_prefix)
241 assert re_file_safe.match(fn1)
243 fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
244 try:
245 fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
246 with os.fdopen(fh, "wt") as f:
247 f.write("NOTOK\n")
248 f.write(
249 "From: {0} <{1}>\n\n".format(
250 utils.whoami(), cnf["Dinstall::MyAdminAddress"]
251 )
252 )
253 f.write(reason)
254 except OSError as e:
255 if e.errno == errno.EEXIST:
256 pass
257 else:
258 raise
260 def get_action(self) -> Optional[str]:
261 """get current action
263 :return: string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT'
264 """
265 changes_prefix = self._changes_prefix
267 for action in ("ACCEPT", "ACCEPTED", "REJECT"):
268 fn1 = "{0}.{1}".format(action, changes_prefix)
269 fn = os.path.join(self.upload.policy_queue.path, "COMMENTS", fn1)
270 if os.path.exists(fn):
271 return action
273 return None
275 def missing_overrides(
276 self, hints: Optional[list[MissingOverride]] = None
277 ) -> list[MissingOverride]:
278 """get missing override entries for the upload
280 :param hints: suggested hints for new overrides in the same format as
281 the return value
282 :return: list of dicts with the following keys:
284 - package: package name
285 - priority: default priority (from upload)
286 - section: default section (from upload)
287 - component: default component (from upload)
288 - type: type of required override ('dsc', 'deb' or 'udeb')
290 All values are strings.
291 """
292 # TODO: use Package-List field
293 missing: list[MissingOverride] = []
294 components = set()
296 source = self.upload.source
298 if hints is None:
299 hints = []
300 hints_map = dict([((o["type"] or "", o["package"]), o) for o in hints])
302 def check_override(
303 name: str,
304 type: str | None,
305 priority: str | None,
306 section: str | None,
307 included: bool,
308 ) -> None:
309 type = type or ""
310 section = section or ""
311 component = "main"
312 if section.find("/") != -1:
313 component = section.split("/", 1)[0]
314 override = self._binary_override(name, type, component)
315 if override is None and not any(
316 o["package"] == name and o["type"] == type for o in missing
317 ):
318 hint = hints_map.get((type, name))
319 if hint is not None:
320 missing.append(hint)
321 component = hint["component"]
322 else:
323 missing.append(
324 {
325 "package": name,
326 "priority": priority or "",
327 "section": section,
328 "component": component or "",
329 "type": type or "",
330 "included": included,
331 }
332 )
333 components.add(component)
335 for binary in self.upload.binaries:
336 binary_proxy = binary.proxy
337 priority = binary_proxy.get("Priority", "optional")
338 section = binary_proxy["Section"]
339 check_override(
340 binary.package, binary.binarytype, priority, section, included=True
341 )
343 if source is not None:
344 source_proxy = source.proxy
345 package_list = PackageList(source_proxy)
346 if not package_list.fallback: 346 ↛ 356line 346 didn't jump to line 356 because the condition on line 346 was always true
347 packages = package_list.packages_for_suite(self.upload.target_suite)
348 for p in packages:
349 check_override(
350 p.name, p.type, p.priority, p.section, included=False
351 )
353 # see daklib.archive.source_component_from_package_list
354 # which we cannot use here as we might not have a Package-List
355 # field for old packages
356 mapped_components = [get_mapped_component_name(c) for c in components]
357 source_component_db = (
358 self.session.query(Component)
359 .order_by(Component.ordering)
360 .filter(Component.component_name.in_(mapped_components))
361 .first()
362 )
363 assert source_component_db is not None
364 source_component = source_component_db.component_name
366 override = self._source_override(source_component)
367 if override is None:
368 hint = hints_map.get(("dsc", source.source))
369 if hint is not None:
370 missing.append(hint)
371 else:
372 section = "misc"
373 if source_component != "main":
374 section = "{0}/{1}".format(source_component, section)
375 missing.append(
376 dict(
377 package=source.source,
378 priority="optional",
379 section=section,
380 component=source_component,
381 type="dsc",
382 included=True,
383 )
384 )
386 return missing
388 def add_overrides(
389 self, new_overrides: "Iterable[MissingOverride]", suite: Suite
390 ) -> None:
391 if suite.overridesuite is not None:
392 suite = (
393 self.session.query(Suite)
394 .filter_by(suite_name=suite.overridesuite)
395 .one()
396 )
398 for override in new_overrides:
399 package = override["package"]
400 priority = (
401 self.session.query(Priority)
402 .filter_by(priority=override["priority"])
403 .first()
404 )
405 section = (
406 self.session.query(Section)
407 .filter_by(section=override["section"])
408 .first()
409 )
410 component = get_mapped_component(override["component"], self.session)
411 overridetype = (
412 self.session.query(OverrideType)
413 .filter_by(overridetype=override["type"])
414 .one()
415 )
417 if priority is None: 417 ↛ 418line 417 didn't jump to line 418 because the condition on line 417 was never true
418 raise Exception(
419 "Invalid priority {0} for package {1}".format(priority, package)
420 )
421 if section is None: 421 ↛ 422line 421 didn't jump to line 422 because the condition on line 421 was never true
422 raise Exception(
423 "Invalid section {0} for package {1}".format(section, package)
424 )
425 if component is None: 425 ↛ 426line 425 didn't jump to line 426 because the condition on line 425 was never true
426 raise Exception(
427 "Invalid component {0} for package {1}".format(component, package)
428 )
430 o = Override(
431 package=package,
432 suite=suite,
433 component=component,
434 priority=priority,
435 section=section,
436 overridetype=overridetype,
437 )
438 self.session.add(o)
440 self.session.commit()