Coverage for daklib/policy.py: 88%

195 statements  

« 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. 

16 

17"""module to process policy queue uploads""" 

18 

19import errno 

20import os 

21import shutil 

22from typing import TYPE_CHECKING, NotRequired, Optional, TypedDict 

23 

24import daklib.utils as utils 

25 

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 

41 

42if TYPE_CHECKING: 

43 from collections.abc import Iterable 

44 

45 from sqlalchemy.orm import Session 

46 

47 

48class UploadCopy: 

49 """export a policy queue upload 

50 

51 This class can be used in a with-statement:: 

52 

53 with UploadCopy(...) as copy: 

54 ... 

55 

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 """ 

60 

61 def __init__(self, upload: PolicyQueueUpload, group: Optional[str] = None): 

62 """initializer 

63 

64 :param upload: upload to handle 

65 """ 

66 

67 self._directory: Optional[str] = None 

68 self.upload = upload 

69 self.group = group 

70 

71 @property 

72 def directory(self) -> str: 

73 assert self._directory is not None 

74 return self._directory 

75 

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 

84 

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 

93 

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) 

100 

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) 

106 

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) 

115 

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) 

121 

122 def __enter__(self): 

123 assert self._directory is None 

124 

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 

130 

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 

137 

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 

143 

144 

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] 

153 

154 

155class PolicyQueueUploadHandler: 

156 """process uploads to policy queues 

157 

158 This class allows to accept or reject uploads and to get a list of missing 

159 overrides (for NEW processing). 

160 """ 

161 

162 def __init__(self, upload: PolicyQueueUpload, session: "Session"): 

163 """initializer 

164 

165 :param upload: upload to process 

166 :param session: database session 

167 """ 

168 self.upload = upload 

169 self.session = session 

170 

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 

181 

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() 

195 

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() 

209 

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] 

216 

217 def accept(self) -> None: 

218 """mark upload as accepted""" 

219 assert len(self.missing_overrides()) == 0 

220 

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 

232 

233 def reject(self, reason: str) -> None: 

234 """mark upload as rejected 

235 

236 :param reason: reason for the rejection 

237 """ 

238 cnf = Config() 

239 

240 fn1 = "REJECT.{0}".format(self._changes_prefix) 

241 assert re_file_safe.match(fn1) 

242 

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 

259 

260 def get_action(self) -> Optional[str]: 

261 """get current action 

262 

263 :return: string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT' 

264 """ 

265 changes_prefix = self._changes_prefix 

266 

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 

272 

273 return None 

274 

275 def missing_overrides( 

276 self, hints: Optional[list[MissingOverride]] = None 

277 ) -> list[MissingOverride]: 

278 """get missing override entries for the upload 

279 

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: 

283 

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') 

289 

290 All values are strings. 

291 """ 

292 # TODO: use Package-List field 

293 missing: list[MissingOverride] = [] 

294 components = set() 

295 

296 source = self.upload.source 

297 

298 if hints is None: 

299 hints = [] 

300 hints_map = dict([((o["type"] or "", o["package"]), o) for o in hints]) 

301 

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) 

334 

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 ) 

342 

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 ) 

352 

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 

365 

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 ) 

385 

386 return missing 

387 

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 ) 

397 

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 ) 

416 

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 ) 

429 

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) 

439 

440 self.session.commit()