]> git.decadent.org.uk Git - dak.git/blob - dak/update_suite.py
Merge remote-tracking branch 'nthykier/auto-decruft'
[dak.git] / dak / update_suite.py
1 #! /usr/bin/env python
2 #
3 # Copyright (C) 2015, Ansgar Burchardt <ansgar@debian.org>
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19 from daklib.archive import ArchiveTransaction
20 from daklib.dbconn import *
21 import daklib.daklog
22 import daklib.utils
23
24 from sqlalchemy.orm.exc import NoResultFound
25 import sys
26
27 """
28 Idea:
29
30 dak update-suite testing testing-kfreebsd
31  -> grab all source & binary packages from testing with a higher version
32     than in testing-kfreebsd (or not in -kfreebsd) and copy them
33  -> limited to architectures in testing-kfreebsd
34  -> obeys policy queues
35  -> copies to build queues
36
37 dak update-suite --create-in=ftp-master stable testing
38  -> create suite "testing" based on "stable" in archive "ftp-master"
39
40 Additional switches:
41  --skip-policy-queue:    skip target suite's policy queue
42  --skip-build-queues:    do not copy to build queue
43  --no-new-packages:      do not copy new packages
44                          -> source-based, new binaries from existing sources will be added
45  --only-new-packages:    do not update existing packages
46                          -> source-based, do not copy new binaries w/o source!
47  --also-policy-queue:    also copy pending packages from policy queue
48  --update-overrides:     update overrides as well (if different overrides are used)
49  --no-act
50 """
51
52 def usage():
53     print("dak update-suite [-n|--no-act] <origin> <target>")
54     sys.exit(0)
55
56 class SuiteUpdater(object):
57     def __init__(self, transaction, origin, target,
58                  new_packages=True, also_from_policy_queue=False,
59                  obey_policy_queue=True, obey_build_queues=True,
60                  update_overrides=False, dry_run=False):
61         self.transaction = transaction
62         self.origin = origin
63         self.target = target
64         self.new_packages = new_packages
65         self.also_from_policy_queue = also_from_policy_queue
66         self.obey_policy_queue = obey_policy_queue
67         self.obey_build_queues = obey_build_queues
68         self.update_overrides = update_overrides
69         self.dry_run = dry_run
70
71         if obey_policy_queue and target.policy_queue_id is not None:
72             raise Exception('Not implemented...')
73         self.logger = None if dry_run else daklib.daklog.Logger("update-suite")
74
75     def query_new_binaries(self, additional_sources):
76         # Candidates are binaries in the origin suite, and optionally in its policy queue.
77         query = """
78           SELECT b.*
79           FROM binaries b
80           JOIN bin_associations ba ON b.id = ba.bin AND ba.suite = :origin
81         """
82         if self.also_from_policy_queue:
83             query += """
84               UNION
85               SELECT b.*
86               FROM binaries b
87               JOIN policy_queue_upload_binaries_map pqubm ON pqubm.binary_id = b.id
88               JOIN policy_queue_upload pqu ON pqu.id = pqubm.policy_queue_upload_id
89               WHERE pqu.target_suite_id = :origin
90                 AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :origin)
91             """
92
93         # Only take binaries that are for a architecture part of the target suite,
94         # and whose source was just added to the target suite (i.e. listed in additional_sources)
95         #     or that have the source already available in the target suite
96         #     or in the target suite's policy queue if we obey policy queues,
97         # and filter out binaries with a lower version than already in the target suite.
98         if self.obey_policy_queue:
99             cond_source_in_policy_queue = """
100               EXISTS (SELECT 1
101                       FROM policy_queue_upload pqu
102                       WHERE tmp.source = pqu.source_id
103                         AND pqu.target_suite_id = :target
104                         AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :target))
105             """
106         else:
107             cond_source_in_policy_queue = "FALSE"
108         query = """
109           WITH tmp AS ({0})
110           SELECT DISTINCT *
111           FROM tmp
112           WHERE tmp.architecture IN (SELECT architecture FROM suite_architectures WHERE suite = :target)
113             AND (tmp.source IN :additional_sources
114                  OR EXISTS (SELECT 1
115                             FROM src_associations sa
116                             WHERE tmp.source = sa.source AND sa.suite = :target)
117                  OR {1})
118             AND NOT EXISTS (SELECT 1
119                             FROM binaries b2
120                             JOIN bin_associations ba2 ON b2.id = ba2.bin AND ba2.suite = :target
121                             WHERE tmp.package = b2.package AND tmp.architecture = b2.architecture AND b2.version >= tmp.version)
122           ORDER BY package, version, architecture
123         """.format(query, cond_source_in_policy_queue)
124
125         # An empty tuple generates a SQL statement with "tmp.source IN ()"
126         # which is not valid. Inject an invalid value in this case:
127         # "tmp.source IN (NULL)" is always false.
128         if len(additional_sources) == 0:
129             additional_sources = tuple([None])
130
131         params = {
132             'origin': self.origin.suite_id,
133             'target': self.target.suite_id,
134             'additional_sources': additional_sources,
135         }
136
137         return self.transaction.session.query(DBBinary).from_statement(query).params(params)
138
139     def query_new_sources(self):
140         # Candidates are source packages in the origin suite, and optionally in its policy queue.
141         query = """
142           SELECT s.*
143           FROM source s
144           JOIN src_associations sa ON s.id = sa.source AND sa.suite = :origin
145         """
146         if self.also_from_policy_queue:
147             query += """
148               UNION
149               SELECT s.*
150               FROM source s
151               JOIN policy_queue_upload pqu ON pqu.source_id = s.id
152               WHERE pqu.target_suite_id = :origin
153                 AND pqu.policy_queue_id = (SELECT policy_queue_id FROM suite WHERE id = :origin)
154             """
155
156         # Filter out source packages with a lower version than already in the target suite.
157         query = """
158           WITH tmp AS ({0})
159           SELECT DISTINCT *
160           FROM tmp
161           WHERE NOT EXISTS (SELECT 1
162                             FROM source s2
163                             JOIN src_associations sa2 ON s2.id = sa2.source AND sa2.suite = :target
164                             WHERE s2.source = tmp.source AND s2.version >= tmp.version)
165         """.format(query)
166
167         # Optionally filter out source packages that are not already in the target suite.
168         if not self.new_packages:
169             query += """
170               AND EXISTS (SELECT 1
171                           FROM source s2
172                           JOIN src_associations sa2 ON s2.id = sa2.source AND sa2.suite = :target
173                           WHERE s2.source = tmp.source)
174             """
175
176         query += "ORDER BY source, version"
177
178         params = {'origin': self.origin.suite_id, 'target': self.target.suite_id}
179
180         return self.transaction.session.query(DBSource).from_statement(query).params(params)
181
182     def _components_for_binary(self, binary, suite):
183         session = self.transaction.session
184         return session.query(Component) \
185                       .join(ArchiveFile, Component.component_id == ArchiveFile.component_id) \
186                       .join(ArchiveFile.file).filter(PoolFile.file_id == binary.poolfile_id) \
187                       .filter(ArchiveFile.archive_id == suite.archive_id)
188
189     def install_binaries(self, binaries, suite):
190         if len(binaries) == 0:
191             return
192         # If origin and target suites are in the same archive, we can skip the
193         # overhead from ArchiveTransaction.copy_binary()
194         if self.origin.archive_id == suite.archive_id:
195             query = "INSERT INTO bin_associations (bin, suite) VALUES (:bin, :suite)"
196             target_id = suite.suite_id
197             params = [{'bin': b.binary_id, 'suite': target_id} for b in binaries]
198             self.transaction.session.execute(query, params)
199         else:
200             for b in binaries:
201                 for c in self._components_for_binary(b, suite):
202                     self.transaction.copy_binary(b, suite, c)
203
204     def _components_for_source(self, source, suite):
205         session = self.transaction.session
206         return session.query(Component) \
207                       .join(ArchiveFile, Component.component_id == ArchiveFile.component_id) \
208                       .join(ArchiveFile.file).filter(PoolFile.file_id == source.poolfile_id) \
209                       .filter(ArchiveFile.archive_id == suite.archive_id)
210
211     def install_sources(self, sources, suite):
212         if len(sources) == 0:
213             return
214         # If origin and target suites are in the same archive, we can skip the
215         # overhead from ArchiveTransaction.copy_source()
216         if self.origin.archive_id == suite.archive_id:
217             query = "INSERT INTO src_associations (source, suite) VALUES (:source, :suite)"
218             target_id = suite.suite_id
219             params = [{'source': s.source_id, 'suite': target_id} for s in sources]
220             self.transaction.session.execute(query, params)
221         else:
222             for s in sources:
223                 for c in self._components_for_source(s, suite):
224                     self.transaction.copy_source(s, suite, c)
225
226     def update_suite(self):
227         targets = set([self.target])
228         if self.obey_build_queues:
229             targets.update([bq.suite for bq in self.target.copy_queues])
230         target_names = [ s.suite_name for s in targets ]
231         target_names.sort()
232         target_name = ",".join(target_names)
233
234         new_sources = self.query_new_sources().all()
235         additional_sources = tuple(s.source_id for s in new_sources)
236         for s in new_sources:
237             self.log(["add-source", target_name, s.source, s.version])
238         if not self.dry_run:
239             for target in targets:
240                 self.install_sources(new_sources, target)
241
242         new_binaries = self.query_new_binaries(additional_sources).all()
243         for b in new_binaries:
244             self.log(["add-binary", target_name, b.package, b.version, b.architecture.arch_string])
245         if not self.dry_run:
246             for target in targets:
247                 self.install_binaries(new_binaries, target)
248
249     def log(self, args):
250         if self.logger:
251             self.logger.log(args)
252         else:
253             print(args)
254
255 def main():
256     from daklib.config import Config
257     config = Config()
258
259     import apt_pkg
260     arguments = [
261         ('h', 'help', 'Update-Suite::Options::Help'),
262         ('n', 'no-act', 'Update-Suite::options::NoAct'),
263     ]
264     argv = apt_pkg.parse_commandline(config.Cnf, arguments, sys.argv)
265     try:
266         options = config.subtree("Update-Suite::Options")
267     except KeyError:
268         options = {}
269
270     if 'Help' in options or len(argv) != 2:
271         usage()
272
273     origin_name = argv[0]
274     target_name = argv[1]
275     dry_run = True if 'NoAct' in options else False
276
277     with ArchiveTransaction() as transaction:
278         session = transaction.session
279
280         try:
281             origin = session.query(Suite).filter_by(suite_name=origin_name).one()
282         except NoResultFound:
283             daklib.utils.fubar("Origin suite '{0}' is unknown.".format(origin_name))
284         try:
285             target = session.query(Suite).filter_by(suite_name=target_name).one()
286         except NoResultFound:
287             daklib.utils.fubar("Target suite '{0}' is unknown.".format(target_name))
288
289         su = SuiteUpdater(transaction, origin, target, dry_run=dry_run)
290         su.update_suite()
291
292         if dry_run:
293             transaction.rollback()
294         else:
295             transaction.commit()
296
297 if __name__ == '__main__':
298     pass