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