1#! /usr/bin/env python3 

2 

3""" 

4Remove obsolete source and binary associations from suites. 

5 

6@contact: Debian FTP Master <ftpmaster@debian.org> 

7@copyright: 2009 Torsten Werner <twerner@debian.org> 

8@license: GNU General Public License version 2 or later 

9""" 

10 

11# This program is free software; you can redistribute it and/or modify 

12# it under the terms of the GNU General Public License as published by 

13# the Free Software Foundation; either version 2 of the License, or 

14# (at your option) any later version. 

15 

16# This program is distributed in the hope that it will be useful, 

17# but WITHOUT ANY WARRANTY; without even the implied warranty of 

18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19# GNU General Public License for more details. 

20 

21# You should have received a copy of the GNU General Public License 

22# along with this program; if not, write to the Free Software 

23# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

24 

25import sys 

26 

27import apt_pkg 

28from sqlalchemy.sql import exists, text 

29from tabulate import tabulate 

30 

31from daklib import daklog, utils 

32from daklib.config import Config 

33from daklib.dbconn import DBConn, PolicyQueue, Suite 

34 

35Options = None 

36Logger = None 

37 

38 

39def retrieve_associations(suites, session): 

40 return session.execute( 

41 text( 

42 """ 

43WITH 

44 -- Provide (source, suite) tuple of all source packages to remain 

45 remain_source AS ( 

46 SELECT 

47 * 

48 FROM ( 

49 SELECT 

50 source.id AS source_id, 

51 src_associations.suite AS suite_id, 

52 -- generate rank over versions of a source package in one suite 

53 -- "1" being the newest 

54 dense_rank() OVER ( 

55 PARTITION BY source.source, src_associations.suite 

56 ORDER BY source.version DESC 

57 ) AS version_rank 

58 FROM 

59 source 

60 INNER JOIN src_associations ON 

61 src_associations.source = source.id 

62 AND src_associations.suite = ANY(:suite_ids) 

63 ) AS source_ranked 

64 WHERE 

65 version_rank = 1 

66 ), 

67 -- Provide (source, arch, suite) tuple of all binary packages to remain 

68 remain_binaries AS ( 

69 SELECT 

70 * 

71 FROM ( 

72 SELECT 

73 binaries.id, 

74 binaries.architecture AS arch_id, 

75 bin_associations.suite AS suite_id, 

76 source.id AS source_id, 

77 architecture.arch_string AS arch, 

78 -- arch of newest version 

79 first_value(architecture.arch_string) OVER ( 

80 PARTITION BY binaries.package, bin_associations.suite 

81 ORDER BY binaries.version DESC 

82 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 

83 ) as arch_first, 

84 -- generate rank over versions of a source package in one suite 

85 -- "1" being the newest 

86 -- if newest package is arch-any, we use the rank only over current arch 

87 dense_rank() OVER ( 

88 PARTITION BY binaries.package, binaries.architecture, bin_associations.suite 

89 ORDER BY binaries.version DESC 

90 ) AS version_rank_any, 

91 -- if newest package is arch-all, we use the rank over all arches 

92 -- this makes it possible to replace all by any and any by all 

93 dense_rank() OVER ( 

94 PARTITION BY binaries.package, bin_associations.suite 

95 ORDER BY binaries.version DESC 

96 ) AS version_rank_all 

97 FROM 

98 binaries 

99 INNER JOIN source ON source.id = binaries.source 

100 INNER JOIN bin_associations ON 

101 bin_associations.bin = binaries.id 

102 AND bin_associations.suite = ANY(:suite_ids) 

103 INNER JOIN architecture ON architecture.id = binaries.architecture 

104 ) AS source_rank 

105 WHERE 

106 -- we only want to retain the newest of each 

107 CASE 

108 WHEN arch != 'all' AND arch_first != 'all' THEN version_rank_any = 1 

109 ELSE version_rank_all = 1 

110 END 

111 ), 

112 -- Figure out which source we should remove 

113 -- A binary forces the corresponding source to remain 

114 dominate_source AS ( 

115 SELECT 

116 source.source AS source_package, 

117 source.version AS source_version, 

118 source.source AS package, 

119 source.version, 

120 'source'::text AS arch, 

121 suite.suite_name AS suite, 

122 src_associations.id AS assoc_id 

123 FROM 

124 source 

125 INNER JOIN src_associations ON 

126 src_associations.source = source.id 

127 AND src_associations.suite = ANY(:suite_ids) 

128 INNER join suite ON suite.id = src_associations.suite 

129 LEFT JOIN remain_binaries ON 

130 remain_binaries.source_id = source.id 

131 AND remain_binaries.suite_id = src_associations.suite 

132 LEFT JOIN remain_source ON 

133 remain_source.source_id = source.id 

134 AND remain_source.suite_id = src_associations.suite 

135 WHERE 

136 remain_binaries.source_id IS NULL 

137 AND remain_source.source_id IS NULL 

138 ), 

139 -- Figure out which arch-any binaries we should remove 

140 dominate_binaries AS ( 

141 SELECT 

142 source.source AS source_package, 

143 source.version AS source_version, 

144 binaries.package AS package, 

145 binaries.version, 

146 architecture.arch_string AS arch, 

147 suite.suite_name AS suite, 

148 bin_associations.id AS assoc_id 

149 FROM 

150 binaries 

151 INNER JOIN source ON source.id = binaries.source 

152 INNER JOIN bin_associations ON 

153 bin_associations.bin = binaries.id 

154 AND bin_associations.suite = ANY(:suite_ids) 

155 INNER JOIN architecture ON architecture.id = binaries.architecture 

156 INNER join suite ON suite.id = bin_associations.suite 

157 LEFT JOIN remain_binaries ON 

158 remain_binaries.id = binaries.id 

159 AND remain_binaries.arch_id = binaries.architecture 

160 AND remain_binaries.suite_id = bin_associations.suite 

161 WHERE 

162 remain_binaries.source_id IS NULL 

163 AND binaries.architecture != (SELECT id from architecture WHERE arch_string = 'all') 

164 ), 

165 -- Figure out which arch-all binaries we should remove 

166 -- A arch-any binary forces the related arch-all binaries to remain 

167 dominate_binaries_all AS ( 

168 SELECT 

169 source.source AS source_package, 

170 source.version AS source_version, 

171 binaries.package AS package, 

172 binaries.version, 

173 architecture.arch_string AS arch, 

174 suite.suite_name AS suite, 

175 bin_associations.id AS assoc_id 

176 FROM 

177 binaries 

178 INNER JOIN source ON source.id = binaries.source 

179 INNER JOIN bin_associations ON 

180 bin_associations.bin = binaries.id 

181 AND bin_associations.suite = ANY(:suite_ids) 

182 INNER JOIN architecture ON architecture.id = binaries.architecture 

183 INNER join suite ON suite.id = bin_associations.suite 

184 LEFT JOIN remain_binaries ON 

185 remain_binaries.id = binaries.id 

186 AND remain_binaries.arch_id = binaries.architecture 

187 AND remain_binaries.suite_id = bin_associations.suite 

188 LEFT JOIN remain_binaries AS remain_binaries_any ON 

189 remain_binaries_any.source_id = source.id 

190 AND remain_binaries_any.suite_id = bin_associations.suite 

191 AND remain_binaries_any.arch_id != (SELECT id from architecture WHERE arch_string = 'all') 

192 WHERE 

193 remain_binaries.source_id IS NULL 

194 AND remain_binaries_any.source_id IS NULL 

195 AND binaries.architecture = (SELECT id from architecture WHERE arch_string = 'all') 

196 ) 

197SELECT 

198 * 

199 FROM 

200 dominate_source 

201 UNION SELECT 

202 * 

203 FROM 

204 dominate_binaries 

205 UNION SELECT 

206 * 

207 FROM 

208 dominate_binaries_all 

209 ORDER BY 

210 source_package, source_version, package, version, arch, suite 

211""" 

212 ).params( 

213 suite_ids=[s.suite_id for s in suites], 

214 ) 

215 ) 

216 

217 

218def delete_associations_table(table, ids, session): 

219 result = session.execute( 

220 text( 

221 """ 

222 DELETE 

223 FROM {} 

224 WHERE id = ANY(:assoc_ids) 

225 """.format( 

226 table 

227 ) 

228 ).params( 

229 assoc_ids=list(ids), 

230 ) 

231 ) 

232 

233 assert result.rowcount == len( 

234 ids 

235 ), "Rows deleted are not equal to deletion requests" 

236 

237 

238def delete_associations(assocs, session): 

239 ids_bin = set() 

240 ids_src = set() 

241 

242 for e in assocs: 

243 Logger.log(["newer", e.package, e.version, e.suite, e.arch, e.assoc_id]) 

244 

245 if e.arch == "source": 

246 ids_src.add(e.assoc_id) 

247 else: 

248 ids_bin.add(e.assoc_id) 

249 

250 delete_associations_table("bin_associations", ids_bin, session) 

251 delete_associations_table("src_associations", ids_src, session) 

252 

253 

254def usage(): 

255 print( 

256 """Usage: dak dominate [OPTIONS] 

257Remove obsolete source and binary associations from suites. 

258 

259 -s, --suite=SUITE act on this suite 

260 -h, --help show this help and exit 

261 -n, --no-action don't commit changes 

262 -f, --force also clean up untouchable suites 

263 

264SUITE can be comma (or space) separated list, e.g. 

265 --suite=testing,unstable""" 

266 ) 

267 sys.exit() 

268 

269 

270def main(): 

271 global Options, Logger 

272 cnf = Config() 

273 Arguments = [ 

274 ("h", "help", "Obsolete::Options::Help"), 

275 ("s", "suite", "Obsolete::Options::Suite", "HasArg"), 

276 ("n", "no-action", "Obsolete::Options::No-Action"), 

277 ("f", "force", "Obsolete::Options::Force"), 

278 ] 

279 cnf["Obsolete::Options::Help"] = "" 

280 cnf["Obsolete::Options::No-Action"] = "" 

281 cnf["Obsolete::Options::Force"] = "" 

282 apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

283 Options = cnf.subtree("Obsolete::Options") 

284 if Options["Help"]: 

285 usage() 

286 

287 if not Options["No-Action"]: 287 ↛ 289line 287 didn't jump to line 289, because the condition on line 287 was never false

288 Logger = daklog.Logger("dominate") 

289 session = DBConn().session() 

290 

291 suites_query = ( 

292 session.query(Suite) 

293 .order_by(Suite.suite_name) 

294 .filter(~exists().where(Suite.suite_id == PolicyQueue.suite_id)) 

295 ) 

296 if "Suite" in Options: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true

297 suites_query = suites_query.filter( 

298 Suite.suite_name.in_(utils.split_args(Options["Suite"])) 

299 ) 

300 if not Options["Force"]: 300 ↛ 302line 300 didn't jump to line 302, because the condition on line 300 was never false

301 suites_query = suites_query.filter_by(untouchable=False) 

302 suites = suites_query.all() 

303 

304 assocs = list(retrieve_associations(suites, session)) 

305 

306 if Options["No-Action"]: 306 ↛ 307line 306 didn't jump to line 307, because the condition on line 306 was never true

307 headers = ( 

308 "source package", 

309 "source version", 

310 "package", 

311 "version", 

312 "arch", 

313 "suite", 

314 "id", 

315 ) 

316 print((tabulate(assocs, headers, tablefmt="orgtbl"))) 

317 session.rollback() 

318 

319 else: 

320 delete_associations(assocs, session) 

321 session.commit() 

322 

323 if Logger: 323 ↛ exitline 323 didn't return from function 'main', because the condition on line 323 was never false

324 Logger.close() 

325 

326 

327if __name__ == "__main__": 

328 main()