]> git.decadent.org.uk Git - dak.git/blob - daklib/policy.py
Add module to process policy queue uploads.
[dak.git] / daklib / policy.py
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
19 from .config import Config
20 from .dbconn import BinaryMetadata, Component, MetadataKey, Override, OverrideType
21 from .fstransactions import FilesystemTransaction
22 from .regexes import re_file_changes, re_file_safe
23
24 import errno
25 import os
26 import shutil
27 import tempfile
28
29 class UploadCopy(object):
30     """export a policy queue upload
31
32     This class can be used in a with-statements:
33
34        with UploadCopy(...) as copy:
35           ...
36
37     Doing so will provide a temporary copy of the upload in the directory
38     given by the `directory` attribute.  The copy will be removed on leaving
39     the with-block.
40
41     Args:
42        upload (daklib.dbconn.PolicyQueueUpload)
43     """
44     def __init__(self, upload):
45         self.directory = None
46         self.upload = upload
47
48     def export(self, directory, mode=None, symlink=True):
49         """export a copy of the upload
50
51         Args:
52            directory (str)
53
54         Kwargs:
55            mode (int): permissions to use for the copied files
56            symlink (bool): use symlinks instead of copying the files (default: True)
57         """
58         with FilesystemTransaction() as fs:
59             source = self.upload.source
60             queue = self.upload.policy_queue
61
62             if source is not None:
63                 for dsc_file in source.srcfiles:
64                     f = dsc_file.poolfile
65                     dst = os.path.join(directory, os.path.basename(f.filename))
66                     fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
67             for binary in self.upload.binaries:
68                 f = binary.poolfile
69                 dst = os.path.join(directory, os.path.basename(f.filename))
70                 fs.copy(f.fullpath, dst, mode=mode, symlink=symlink)
71
72             # copy byhand files
73             for byhand in self.upload.byhand:
74                 src = os.path.join(queue.path, byhand.filename)
75                 dst = os.path.join(directory, byhand.filename)
76                 fs.copy(src, dst, mode=mode, symlink=symlink)
77
78             # copy .changes
79             src = os.path.join(queue.path, self.upload.changes.changesname)
80             dst = os.path.join(directory, self.upload.changes.changesname)
81             fs.copy(src, dst, mode=mode, symlink=symlink)
82
83     def __enter__(self):
84         assert self.directory is None
85
86         cnf = Config()
87         self.directory = tempfile.mkdtemp(dir=cnf.get('Dir::TempPath'))
88         self.export(self.directory, symlink=True)
89         return self
90
91     def __exit__(self, *args):
92         if self.directory is not None:
93             shutil.rmtree(self.directory)
94             self.directory = None
95         return None
96
97 class PolicyQueueUploadHandler(object):
98     """process uploads to policy queues
99
100     This class allows to accept or reject uploads and to get a list of missing
101     overrides (for NEW processing).
102     """
103     def __init__(self, upload, session):
104         """initializer
105
106         Args:
107            upload (daklib.dbconn.PolicyQueueUpload): upload to process
108            session: database session
109         """
110         self.upload = upload
111         self.session = session
112
113     @property
114     def _overridesuite(self):
115         overridesuite = self.upload.target_suite
116         if overridesuite.overridesuite is not None:
117             overridesuite = self.session.query(Suite).filter_by(suite_name=overridesuite.overridesuite).one()
118         return overridesuite
119
120     def _source_override(self, component_name):
121         package = self.upload.source.source
122         suite = self._overridesuite
123         query = self.session.query(Override).filter_by(package=package, suite=suite) \
124             .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
125             .join(Component).filter(Component.component_name == component_name)
126         return query.first()
127
128     def _binary_override(self, binary, component_name):
129         package = binary.package
130         suite = self._overridesuite
131         overridetype = binary.binarytype
132         query = self.session.query(Override).filter_by(package=package, suite=suite) \
133             .join(OverrideType).filter(OverrideType.overridetype == overridetype) \
134             .join(Component).filter(Component.component_name == component_name)
135         return query.first()
136
137     def _binary_metadata(self, binary, key):
138         metadata_key = self.session.query(MetadataKey).filter_by(key=key).first()
139         if metadata_key is None:
140             return None
141         metadata = self.session.query(BinaryMetadata).filter_by(binary=binary, key=metadata_key).first()
142         if metadata is None:
143             return None
144         return metadata.value
145
146     @property
147     def _changes_prefix(self):
148         changesname = self.upload.changes.changesname
149         assert changesname.endswith('.changes')
150         assert re_file_changes.match(changesname)
151         return changesname[0:-8]
152
153     def accept(self):
154         """mark upload as accepted"""
155         assert len(self.missing_overrides()) == 0
156
157         fn1 = 'ACCEPT.{0}'.format(self._changes_prefix)
158         fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
159         try:
160             fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
161             os.write(fh, 'OK\n')
162             os.close(fh)
163         except OSError as e:
164             if e.errno == errno.EEXIST:
165                 pass
166             else:
167                 raise
168
169     def reject(self, reason):
170         """mark upload as rejected
171
172         Args:
173            reason (str): reason for the rejection
174         """
175         fn1 = 'REJECT.{0}'.format(self._changes_prefix)
176         assert re_file_safe.match(fn1)
177
178         fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
179         try:
180             fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
181             os.write(fh, 'NOTOK\n')
182             os.write(fh, reason)
183             os.close(fh)
184         except OSError as e:
185             if e.errno == errno.EEXIST:
186                 pass
187             else:
188                 raise
189
190     def get_action(self):
191         """get current action
192
193         Returns:
194            string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT'
195         """
196         changes_prefix = self._changes_prefix
197
198         for action in ('ACCEPT', 'ACCEPTED', 'REJECT'):
199             fn1 = '{0}.{1}'.format(action, changes_prefix)
200             fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
201             if os.path.exists(fn):
202                 return action
203
204         return None
205
206     def missing_overrides(self, hints=None):
207         """get missing override entries for the upload
208
209         Kwargs:
210            hints (list of dict): suggested hints for new overrides in the same
211               format as the return value
212
213         Returns:
214            list of dicts with the following keys:
215               package: package name
216               priority: default priority (from upload)
217               section: default section (from upload)
218               component: default component (from upload)
219               type: type of required override ('dsc', 'deb' or 'udeb')
220            All values are strings.
221         """
222         # TODO: use Package-List field
223         missing = []
224         components = set()
225
226         if hints is None:
227             hints = []
228         hints_map = dict([ ((o['type'], o['package']), o) for o in hints ])
229
230         for binary in self.upload.binaries:
231             priority = self._binary_metadata(binary, 'Priority')
232             section = self._binary_metadata(binary, 'Section')
233             component = 'main'
234             if section.find('/') != -1:
235                 component = section.split('/', 1)[0]
236             override = self._binary_override(binary, component)
237             if override is None:
238                 hint = hints_map.get((binary.binarytype, binary.package))
239                 if hint is not None:
240                     missing.append(hint)
241                     component = hint['component']
242                 else:
243                     missing.append(dict(
244                             package = binary.package,
245                             priority = priority,
246                             section = section,
247                             component = component,
248                             type = binary.binarytype,
249                             ))
250             components.add(component)
251
252         source_component = '(unknown)'
253         for component in ('main', 'contrib', 'non-free'):
254             if component in components:
255                 source_component = component
256                 break
257
258         source = self.upload.source
259         if source is not None:
260             override = self._source_override(source_component)
261             if override is None:
262                 hint = hints_map.get(('dsc', source.source))
263                 if hint is not None:
264                     missing.append(hint)
265                 else:
266                     section = 'misc'
267                     if component != 'main':
268                         section = "{0}/{1}".format(component, section)
269                     missing.append(dict(
270                             package = source.source,
271                             priority = 'extra',
272                             section = section,
273                             component = source_component,
274                             type = 'dsc',
275                             ))
276
277         return missing