]> git.decadent.org.uk Git - dak.git/blob - policy.py
78d58d67aaa2f1cb5ced663e8b7365cbabca1e0e
[dak.git] / 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-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         query = self.session.query(Override).filter_by(package=package, suite=suite) \
130             .join(OverrideType).filter(OverrideType.overridetype == 'dsc') \
131             .join(Component).filter(Component.component_name == component_name)
132         return query.first()
133
134     def _binary_override(self, binary, component_name):
135         package = binary.package
136         suite = self._overridesuite
137         overridetype = binary.binarytype
138         query = self.session.query(Override).filter_by(package=package, suite=suite) \
139             .join(OverrideType).filter(OverrideType.overridetype == overridetype) \
140             .join(Component).filter(Component.component_name == component_name)
141         return query.first()
142
143     def _binary_metadata(self, binary, key):
144         metadata_key = self.session.query(MetadataKey).filter_by(key=key).first()
145         if metadata_key is None:
146             return None
147         metadata = self.session.query(BinaryMetadata).filter_by(binary=binary, key=metadata_key).first()
148         if metadata is None:
149             return None
150         return metadata.value
151
152     @property
153     def _changes_prefix(self):
154         changesname = self.upload.changes.changesname
155         assert changesname.endswith('.changes')
156         assert re_file_changes.match(changesname)
157         return changesname[0:-8]
158
159     def accept(self):
160         """mark upload as accepted"""
161         assert len(self.missing_overrides()) == 0
162
163         fn1 = 'ACCEPT.{0}'.format(self._changes_prefix)
164         fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
165         try:
166             fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644)
167             os.write(fh, 'OK\n')
168             os.close(fh)
169         except OSError as e:
170             if e.errno == errno.EEXIST:
171                 pass
172             else:
173                 raise
174
175     def reject(self, reason):
176         """mark upload as rejected
177
178         @type  reason: str
179         @param reason: reason for the rejection
180         """
181         fn1 = 'REJECT.{0}'.format(self._changes_prefix)
182         assert re_file_safe.match(fn1)
183
184         fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
185         try:
186             fh = os.open(fn, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
187             os.write(fh, 'NOTOK\n')
188             os.write(fh, reason)
189             os.close(fh)
190         except OSError as e:
191             if e.errno == errno.EEXIST:
192                 pass
193             else:
194                 raise
195
196     def get_action(self):
197         """get current action
198
199         @rtype:  str
200         @return: string giving the current action, one of 'ACCEPT', 'ACCEPTED', 'REJECT'
201         """
202         changes_prefix = self._changes_prefix
203
204         for action in ('ACCEPT', 'ACCEPTED', 'REJECT'):
205             fn1 = '{0}.{1}'.format(action, changes_prefix)
206             fn = os.path.join(self.upload.policy_queue.path, 'COMMENTS', fn1)
207             if os.path.exists(fn):
208                 return action
209
210         return None
211
212     def missing_overrides(self, hints=None):
213         """get missing override entries for the upload
214
215         @type  hints: list of dict
216         @param hints: suggested hints for new overrides in the same format as
217                       the return value
218
219         @return: list of dicts with the following keys:
220
221                  - package: package name
222                  - priority: default priority (from upload)
223                  - section: default section (from upload)
224                  - component: default component (from upload)
225                  - type: type of required override ('dsc', 'deb' or 'udeb')
226
227                  All values are strings.
228         """
229         # TODO: use Package-List field
230         missing = []
231         components = set()
232
233         if hints is None:
234             hints = []
235         hints_map = dict([ ((o['type'], o['package']), o) for o in hints ])
236
237         for binary in self.upload.binaries:
238             priority = self._binary_metadata(binary, 'Priority')
239             section = self._binary_metadata(binary, 'Section')
240             component = 'main'
241             if section.find('/') != -1:
242                 component = section.split('/', 1)[0]
243             override = self._binary_override(binary, component)
244             if override is None:
245                 hint = hints_map.get((binary.binarytype, binary.package))
246                 if hint is not None:
247                     missing.append(hint)
248                     component = hint['component']
249                 else:
250                     missing.append(dict(
251                             package = binary.package,
252                             priority = priority,
253                             section = section,
254                             component = component,
255                             type = binary.binarytype,
256                             ))
257             components.add(component)
258
259         source_component = '(unknown)'
260         for component in ('main', 'contrib', 'non-free'):
261             if component in components:
262                 source_component = component
263                 break
264
265         source = self.upload.source
266         if source is not None:
267             override = self._source_override(source_component)
268             if override is None:
269                 hint = hints_map.get(('dsc', source.source))
270                 if hint is not None:
271                     missing.append(hint)
272                 else:
273                     section = 'misc'
274                     if component != 'main':
275                         section = "{0}/{1}".format(component, section)
276                     missing.append(dict(
277                             package = source.source,
278                             priority = 'extra',
279                             section = section,
280                             component = source_component,
281                             type = 'dsc',
282                             ))
283
284         return missing