Coverage for dak/update_suite.py: 78%

125 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2026-08-03 16:46 +0000

1# Copyright (C) 2015, 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 

17import sys 

18from collections.abc import Collection, Sequence 

19from typing import TYPE_CHECKING, NoReturn 

20 

21from sqlalchemy import select, sql 

22 

23import daklib.daklog 

24import daklib.utils 

25from daklib.archive import ArchiveTransaction 

26from daklib.dbconn import ArchiveFile, Component, DBBinary, DBSource, PoolFile, Suite 

27 

28if TYPE_CHECKING: 

29 from sqlalchemy.engine import ScalarResult 

30 

31""" 

32Idea: 

33 

34dak update-suite testing testing-kfreebsd 

35 -> grab all source & binary packages from testing with a higher version 

36 than in testing-kfreebsd (or not in -kfreebsd) and copy them 

37 -> limited to architectures in testing-kfreebsd 

38 -> obeys policy queues 

39 -> copies to build queues 

40 

41dak update-suite --create-in=ftp-master stable testing 

42 -> create suite "testing" based on "stable" in archive "ftp-master" 

43 

44Additional switches: 

45 --skip-policy-queue: skip target suite's policy queue 

46 --skip-build-queues: do not copy to build queue 

47 --no-new-packages: do not copy new packages 

48 -> source-based, new binaries from existing sources will be added 

49 --only-new-packages: do not update existing packages 

50 -> source-based, do not copy new binaries w/o source! 

51 --also-policy-queue: also copy pending packages from policy queue 

52 --update-overrides: update overrides as well (if different overrides are used) 

53 --no-act 

54""" 

55 

56 

57def usage() -> NoReturn: 

58 print("dak update-suite [-n|--no-act] <origin> <target>") 

59 sys.exit(0) 

60 

61 

62class SuiteUpdater: 

63 def __init__( 

64 self, 

65 transaction: ArchiveTransaction, 

66 origin: Suite, 

67 target: Suite, 

68 new_packages=True, 

69 also_from_policy_queue=False, 

70 obey_policy_queue=True, 

71 obey_build_queues=True, 

72 update_overrides=False, 

73 dry_run=False, 

74 ): 

75 self.transaction = transaction 

76 self.origin = origin 

77 self.target = target 

78 self.new_packages = new_packages 

79 self.also_from_policy_queue = also_from_policy_queue 

80 self.obey_policy_queue = obey_policy_queue 

81 self.obey_build_queues = obey_build_queues 

82 self.update_overrides = update_overrides 

83 self.dry_run = dry_run 

84 

85 if obey_policy_queue and target.policy_queue_id is not None: 85 ↛ 86line 85 didn't jump to line 86 because the condition on line 85 was never true

86 raise Exception("Not implemented...") 

87 self.logger = None if dry_run else daklib.daklog.Logger("update-suite") 

88 

89 def query_new_binaries( 

90 self, additional_sources: Collection[int] 

91 ) -> "Sequence[DBBinary]": 

92 # Candidates are binaries in the origin suite, and optionally in its policy queue. 

93 query = """ 

94 SELECT b.* 

95 FROM binaries b 

96 JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :origin 

97 """ 

98 if self.also_from_policy_queue: 98 ↛ 99line 98 didn't jump to line 99 because the condition on line 98 was never true

99 query += """ 

100 UNION 

101 SELECT b.* 

102 FROM binaries b 

103 JOIN policy_queue_upload_binaries_map pqubm ON pqubm.binary_id = b.id 

104 JOIN policy_queue_upload pqu ON pqu.id = pqubm.policy_queue_upload_id 

105 WHERE pqu.target_suite_id = :origin 

106 AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :origin) 

107 """ 

108 

109 # Only take binaries that are for a architecture part of the target suite, 

110 # and whose source was just added to the target suite (i.e. listed in additional_sources) 

111 # or that have the source already available in the target suite 

112 # or in the target suite's policy queue if we obey policy queues, 

113 # and filter out binaries with a lower version than already in the target suite. 

114 if self.obey_policy_queue: 114 ↛ 123line 114 didn't jump to line 123 because the condition on line 114 was always true

115 cond_source_in_policy_queue = """ 

116 EXISTS (SELECT 1 

117 FROM policy_queue_upload pqu 

118 WHERE tmp.source = pqu.source_id 

119 AND pqu.target_suite_id = :target 

120 AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :target)) 

121 """ 

122 else: 

123 cond_source_in_policy_queue = "FALSE" 

124 query = """ 

125 WITH tmp AS ({0}) 

126 SELECT DISTINCT * 

127 FROM tmp 

128 WHERE tmp.architecture IN (SELECT architecture FROM suite_architectures WHERE suite = :target) 

129 AND (tmp.source IN :additional_sources 

130 OR EXISTS (SELECT 1 

131 FROM src_associations sa 

132 WHERE tmp.source = sa.source AND sa.suite = :target) 

133 OR {1}) 

134 AND NOT EXISTS (SELECT 1 

135 FROM binaries b2 

136 JOIN bin_associations ba2 ON b2.id = ba2.bin AND ba2.suite = :target 

137 WHERE tmp.package = b2.package AND tmp.architecture = b2.architecture AND b2.version >= tmp.version) 

138 ORDER BY package, version, architecture 

139 """.format( 

140 query, cond_source_in_policy_queue 

141 ) 

142 

143 # An empty tuple generates a SQL statement with "tmp.source IN ()" 

144 # which is not valid. Inject an invalid value in this case: 

145 # "tmp.source IN (NULL)" is always false. 

146 

147 params = { 

148 "origin": self.origin.suite_id, 

149 "target": self.target.suite_id, 

150 "additional_sources": additional_sources or (None,), 

151 } 

152 

153 return self.transaction.session.scalars( 

154 select(DBBinary).from_statement(sql.text(query)), params 

155 ).all() 

156 

157 def query_new_sources(self) -> "Sequence[DBSource]": 

158 # Candidates are source packages in the origin suite, and optionally in its policy queue. 

159 query = """ 

160 SELECT s.* 

161 FROM source s 

162 JOIN src_associations sa ON s.id = sa.source AND sa.suite = :origin 

163 """ 

164 if self.also_from_policy_queue: 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true

165 query += """ 

166 UNION 

167 SELECT s.* 

168 FROM source s 

169 JOIN policy_queue_upload pqu ON pqu.source_id = s.id 

170 WHERE pqu.target_suite_id = :origin 

171 AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :origin) 

172 """ 

173 

174 # Filter out source packages with a lower version than already in the target suite. 

175 query = """ 

176 WITH tmp AS ({0}) 

177 SELECT DISTINCT * 

178 FROM tmp 

179 WHERE NOT EXISTS (SELECT 1 

180 FROM source s2 

181 JOIN src_associations sa2 ON s2.id = sa2.source AND sa2.suite = :target 

182 WHERE s2.source = tmp.source AND s2.version >= tmp.version) 

183 """.format( 

184 query 

185 ) 

186 

187 # Optionally filter out source packages that are not already in the target suite. 

188 if not self.new_packages: 188 ↛ 189line 188 didn't jump to line 189 because the condition on line 188 was never true

189 query += """ 

190 AND EXISTS (SELECT 1 

191 FROM source s2 

192 JOIN src_associations sa2 ON s2.id = sa2.source AND sa2.suite = :target 

193 WHERE s2.source = tmp.source) 

194 """ 

195 

196 query += "ORDER BY source, version" 

197 

198 params = {"origin": self.origin.suite_id, "target": self.target.suite_id} 

199 

200 return self.transaction.session.scalars( 

201 select(DBSource).from_statement(sql.text(query)), params 

202 ).all() 

203 

204 def _components_for_binary( 

205 self, binary: DBBinary, suite: Suite 

206 ) -> "ScalarResult[Component]": 

207 session = self.transaction.session 

208 return session.scalars( 

209 select(Component) 

210 .join(ArchiveFile, Component.component_id == ArchiveFile.component_id) 

211 .join(ArchiveFile.file) 

212 .where(PoolFile.file_id == binary.poolfile_id) 

213 .where(ArchiveFile.archive_id == suite.archive_id) 

214 ) 

215 

216 def install_binaries(self, binaries: Collection[DBBinary], suite: Suite) -> None: 

217 if len(binaries) == 0: 217 ↛ 218line 217 didn't jump to line 218 because the condition on line 217 was never true

218 return 

219 # If origin and target suites are in the same archive, we can skip the 

220 # overhead from ArchiveTransaction.copy_binary() 

221 if self.origin.archive_id == suite.archive_id: 221 ↛ 227line 221 didn't jump to line 227 because the condition on line 221 was always true

222 query = "INSERT INTO bin_associations (bin, suite) VALUES (:bin, :suite)" 

223 target_id = suite.suite_id 

224 params = [{"bin": b.binary_id, "suite": target_id} for b in binaries] 

225 self.transaction.session.execute(sql.text(query), params) 

226 else: 

227 for b in binaries: 

228 for c in self._components_for_binary(b, suite): 

229 self.transaction.copy_binary(b, suite, c) 

230 

231 def _components_for_source( 

232 self, source: DBSource, suite: Suite 

233 ) -> "ScalarResult[Component]": 

234 session = self.transaction.session 

235 return session.scalars( 

236 select(Component) 

237 .join(ArchiveFile, Component.component_id == ArchiveFile.component_id) 

238 .join(ArchiveFile.file) 

239 .where(PoolFile.file_id == source.poolfile_id) 

240 .where(ArchiveFile.archive_id == suite.archive_id) 

241 ) 

242 

243 def install_sources(self, sources: Collection[DBSource], suite: Suite) -> None: 

244 if len(sources) == 0: 244 ↛ 245line 244 didn't jump to line 245 because the condition on line 244 was never true

245 return 

246 # If origin and target suites are in the same archive, we can skip the 

247 # overhead from ArchiveTransaction.copy_source() 

248 if self.origin.archive_id == suite.archive_id: 248 ↛ 256line 248 didn't jump to line 256 because the condition on line 248 was always true

249 query = ( 

250 "INSERT INTO src_associations (source, suite) VALUES (:source, :suite)" 

251 ) 

252 target_id = suite.suite_id 

253 params = [{"source": s.source_id, "suite": target_id} for s in sources] 

254 self.transaction.session.execute(sql.text(query), params) 

255 else: 

256 for s in sources: 

257 for c in self._components_for_source(s, suite): 

258 self.transaction.copy_source(s, suite, c) 

259 

260 def update_suite(self) -> None: 

261 targets = {self.target} 

262 if self.obey_build_queues: 262 ↛ 264line 262 didn't jump to line 264 because the condition on line 262 was always true

263 targets.update([bq.suite for bq in self.target.copy_queues]) 

264 target_names = sorted(s.suite_name for s in targets) 

265 target_name = ",".join(target_names) 

266 

267 new_sources = self.query_new_sources() 

268 additional_sources = tuple(s.source_id for s in new_sources) 

269 for s in new_sources: 

270 self.log(["add-source", target_name, s.source, s.version]) 

271 if not self.dry_run: 

272 for target in targets: 

273 self.install_sources(new_sources, target) 

274 

275 new_binaries = self.query_new_binaries(additional_sources) 

276 for b in new_binaries: 

277 self.log( 

278 [ 

279 "add-binary", 

280 target_name, 

281 b.package, 

282 b.version, 

283 b.architecture.arch_string, 

284 ] 

285 ) 

286 if not self.dry_run: 

287 for target in targets: 

288 self.install_binaries(new_binaries, target) 

289 

290 def log(self, args: list[object]) -> None: 

291 if self.logger: 

292 self.logger.log(args) 

293 else: 

294 print(*args, sep="|") 

295 

296 

297def main() -> None: 

298 from daklib.config import Config 

299 

300 config = Config() 

301 

302 import apt_pkg 

303 

304 arguments = [ 

305 ("h", "help", "Update-Suite::Options::Help"), 

306 ("n", "no-act", "Update-Suite::options::NoAct"), 

307 ] 

308 argv = apt_pkg.parse_commandline(config.Cnf, arguments, sys.argv) # type: ignore[attr-defined] 

309 try: 

310 options = config.subtree("Update-Suite::Options") 

311 except KeyError: 

312 options = {} 

313 

314 if "Help" in options or len(argv) != 2: 

315 usage() 

316 

317 origin_name = argv[0] 

318 target_name = argv[1] 

319 dry_run = True if "NoAct" in options else False 

320 

321 with ArchiveTransaction() as transaction: 

322 session = transaction.session 

323 

324 origin = session.execute( 

325 select(Suite).filter_by(suite_name=origin_name) 

326 ).scalar_one_or_none() 

327 if origin is None: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 daklib.utils.fubar("Origin suite '{0}' is unknown.".format(origin_name)) 

329 target = session.execute( 

330 select(Suite).filter_by(suite_name=target_name) 

331 ).scalar_one_or_none() 

332 if target is None: 332 ↛ 333line 332 didn't jump to line 333 because the condition on line 332 was never true

333 daklib.utils.fubar("Target suite '{0}' is unknown.".format(target_name)) 

334 

335 su = SuiteUpdater(transaction, origin, target, dry_run=dry_run) 

336 su.update_suite() 

337 

338 if dry_run: 

339 transaction.rollback() 

340 else: 

341 transaction.commit()