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