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