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 

25from daklib.dbconn import * 

26from daklib.config import Config 

27from daklib import daklog, utils 

28import apt_pkg 

29import sys 

30 

31from sqlalchemy.sql import exists, text 

32from tabulate import tabulate 

33 

34 

35Options = None 

36Logger = None 

37 

38 

39def retrieve_associations(suites, session): 

40 return session.execute(text(''' 

41WITH 

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

43 remain_source AS ( 

44 SELECT 

45 * 

46 FROM ( 

47 SELECT 

48 source.id AS source_id, 

49 src_associations.suite AS suite_id, 

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

51 -- "1" being the newest 

52 dense_rank() OVER ( 

53 PARTITION BY source.source, src_associations.suite 

54 ORDER BY source.version DESC 

55 ) AS version_rank 

56 FROM 

57 source 

58 INNER JOIN src_associations ON 

59 src_associations.source = source.id 

60 AND src_associations.suite = ANY(:suite_ids) 

61 ) AS source_ranked 

62 WHERE 

63 version_rank = 1 

64 ), 

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

66 remain_binaries AS ( 

67 SELECT 

68 * 

69 FROM ( 

70 SELECT 

71 binaries.id, 

72 binaries.architecture AS arch_id, 

73 bin_associations.suite AS suite_id, 

74 source.id AS source_id, 

75 architecture.arch_string AS arch, 

76 -- arch of newest version 

77 first_value(architecture.arch_string) OVER ( 

78 PARTITION BY binaries.package, bin_associations.suite 

79 ORDER BY binaries.version DESC 

80 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 

81 ) as arch_first, 

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

83 -- "1" being the newest 

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

85 dense_rank() OVER ( 

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

87 ORDER BY binaries.version DESC 

88 ) AS version_rank_any, 

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

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

91 dense_rank() OVER ( 

92 PARTITION BY binaries.package, bin_associations.suite 

93 ORDER BY binaries.version DESC 

94 ) AS version_rank_all 

95 FROM 

96 binaries 

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

98 INNER JOIN bin_associations ON 

99 bin_associations.bin = binaries.id 

100 AND bin_associations.suite = ANY(:suite_ids) 

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

102 ) AS source_rank 

103 WHERE 

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

105 CASE 

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

107 ELSE version_rank_all = 1 

108 END 

109 ), 

110 -- Figure out which source we should remove 

111 -- A binary forces the corresponding source to remain 

112 dominate_source AS ( 

113 SELECT 

114 source.source AS source_package, 

115 source.version AS source_version, 

116 source.source AS package, 

117 source.version, 

118 'source'::text AS arch, 

119 suite.suite_name AS suite, 

120 src_associations.id AS assoc_id 

121 FROM 

122 source 

123 INNER JOIN src_associations ON 

124 src_associations.source = source.id 

125 AND src_associations.suite = ANY(:suite_ids) 

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

127 LEFT JOIN remain_binaries ON 

128 remain_binaries.source_id = source.id 

129 AND remain_binaries.suite_id = src_associations.suite 

130 LEFT JOIN remain_source ON 

131 remain_source.source_id = source.id 

132 AND remain_source.suite_id = src_associations.suite 

133 WHERE 

134 remain_binaries.source_id IS NULL 

135 AND remain_source.source_id IS NULL 

136 ), 

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

138 dominate_binaries AS ( 

139 SELECT 

140 source.source AS source_package, 

141 source.version AS source_version, 

142 binaries.package AS package, 

143 binaries.version, 

144 architecture.arch_string AS arch, 

145 suite.suite_name AS suite, 

146 bin_associations.id AS assoc_id 

147 FROM 

148 binaries 

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

150 INNER JOIN bin_associations ON 

151 bin_associations.bin = binaries.id 

152 AND bin_associations.suite = ANY(:suite_ids) 

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

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

155 LEFT JOIN remain_binaries ON 

156 remain_binaries.id = binaries.id 

157 AND remain_binaries.arch_id = binaries.architecture 

158 AND remain_binaries.suite_id = bin_associations.suite 

159 WHERE 

160 remain_binaries.source_id IS NULL 

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

162 ), 

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

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

165 dominate_binaries_all AS ( 

166 SELECT 

167 source.source AS source_package, 

168 source.version AS source_version, 

169 binaries.package AS package, 

170 binaries.version, 

171 architecture.arch_string AS arch, 

172 suite.suite_name AS suite, 

173 bin_associations.id AS assoc_id 

174 FROM 

175 binaries 

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

177 INNER JOIN bin_associations ON 

178 bin_associations.bin = binaries.id 

179 AND bin_associations.suite = ANY(:suite_ids) 

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

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

182 LEFT JOIN remain_binaries ON 

183 remain_binaries.id = binaries.id 

184 AND remain_binaries.arch_id = binaries.architecture 

185 AND remain_binaries.suite_id = bin_associations.suite 

186 LEFT JOIN remain_binaries AS remain_binaries_any ON 

187 remain_binaries_any.source_id = source.id 

188 AND remain_binaries_any.suite_id = bin_associations.suite 

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

190 WHERE 

191 remain_binaries.source_id IS NULL 

192 AND remain_binaries_any.source_id IS NULL 

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

194 ) 

195SELECT 

196 * 

197 FROM 

198 dominate_source 

199 UNION SELECT 

200 * 

201 FROM 

202 dominate_binaries 

203 UNION SELECT 

204 * 

205 FROM 

206 dominate_binaries_all 

207 ORDER BY 

208 source_package, source_version, package, version, arch, suite 

209''').params( 

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

211)) 

212 

213 

214def delete_associations_table(table, ids, session): 

215 result = session.execute(text(''' 

216 DELETE 

217 FROM {} 

218 WHERE id = ANY(:assoc_ids) 

219 '''.format(table)).params( 

220 assoc_ids=list(ids), 

221 )) 

222 

223 assert result.rowcount == len(ids), 'Rows deleted are not equal to deletion requests' 

224 

225 

226def delete_associations(assocs, session): 

227 ids_bin = set() 

228 ids_src = set() 

229 

230 for e in assocs: 

231 Logger.log(['newer', e.package, e.version, e.suite, e.arch, e.assoc_id]) 

232 

233 if e.arch == 'source': 

234 ids_src.add(e.assoc_id) 

235 else: 

236 ids_bin.add(e.assoc_id) 

237 

238 delete_associations_table('bin_associations', ids_bin, session) 

239 delete_associations_table('src_associations', ids_src, session) 

240 

241 

242def usage(): 

243 print("""Usage: dak dominate [OPTIONS] 

244Remove obsolete source and binary associations from suites. 

245 

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

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

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

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

250 

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

252 --suite=testing,unstable""") 

253 sys.exit() 

254 

255 

256def main(): 

257 global Options, Logger 

258 cnf = Config() 

259 Arguments = [('h', "help", "Obsolete::Options::Help"), 

260 ('s', "suite", "Obsolete::Options::Suite", "HasArg"), 

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

262 ('f', "force", "Obsolete::Options::Force")] 

263 cnf['Obsolete::Options::Help'] = '' 

264 cnf['Obsolete::Options::No-Action'] = '' 

265 cnf['Obsolete::Options::Force'] = '' 

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

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

268 if Options['Help']: 

269 usage() 

270 

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

272 Logger = daklog.Logger("dominate") 

273 session = DBConn().session() 

274 

275 suites_query = (session 

276 .query(Suite) 

277 .order_by(Suite.suite_name) 

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

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

280 suites_query = suites_query.filter(Suite.suite_name.in_(utils.split_args(Options['Suite']))) 

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

282 suites_query = suites_query.filter_by(untouchable=False) 

283 suites = suites_query.all() 

284 

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

286 

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

288 headers = ('source package', 'source version', 'package', 'version', 'arch', 'suite', 'id') 

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

290 session.rollback() 

291 

292 else: 

293 delete_associations(assocs, session) 

294 session.commit() 

295 

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

297 Logger.close() 

298 

299 

300if __name__ == '__main__': 

301 main()