1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
|
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
""" ChangesFile class
Classes representing Changes and DSC files, which encapsulate collections of
files uploaded.
"""
__metaclass__ = type
__all__ = [
'CannotDetermineFileTypeError',
'ChangesFile',
'determine_file_class_and_name',
]
import os
from lp.archiveuploader.dscfile import (
DSCFile,
SignableTagFile,
)
from lp.archiveuploader.nascentuploadfile import (
BaseBinaryUploadFile,
CustomUploadFile,
DdebBinaryUploadFile,
DebBinaryUploadFile,
SourceUploadFile,
splitComponentAndSection,
UdebBinaryUploadFile,
UploadError,
UploadWarning,
)
from lp.archiveuploader.utils import (
determine_binary_file_type,
determine_source_file_type,
re_changes_file_name,
re_isadeb,
re_issource,
)
from lp.registry.interfaces.sourcepackage import (
SourcePackageFileType,
SourcePackageUrgency,
)
from lp.soyuz.enums import BinaryPackageFileType
class CannotDetermineFileTypeError(Exception):
"""The type of the given file could not be determined."""
class ChangesFile(SignableTagFile):
"""Changesfile model."""
mandatory_fields = set([
"Source", "Binary", "Architecture", "Version", "Distribution",
"Maintainer", "Files", "Changes", "Date",
# Changed-By is not technically mandatory according to
# Debian policy but Soyuz relies on it being set in
# various places.
"Changed-By"])
# Map urgencies to their dbschema values.
# Debian policy only permits low, medium, high, emergency.
# Britney also uses critical which it maps to emergency.
urgency_map = {
"low": SourcePackageUrgency.LOW,
"medium": SourcePackageUrgency.MEDIUM,
"high": SourcePackageUrgency.HIGH,
"critical": SourcePackageUrgency.EMERGENCY,
"emergency": SourcePackageUrgency.EMERGENCY,
}
dsc = None
maintainer = None
changed_by = None
filename_archtag = None
files = None
def __init__(self, filepath, policy, logger):
"""Process the given changesfile.
Does:
* Verification of required fields
* Verification of the required Format
* Parses maintainer and changed-by
* Checks name of changes file
* Checks signature of changes file
If any of these checks fail, UploadError is raised, and it's
considered a fatal error (no subsequent processing of the upload
will be done).
Logger and Policy are instances built in uploadprocessor.py passed
via NascentUpload class.
"""
self.filepath = filepath
self.policy = policy
self.logger = logger
self.parse(verify_signature=not policy.unsigned_changes_ok)
for field in self.mandatory_fields:
if field not in self._dict:
raise UploadError(
"Unable to find mandatory field '%s' in the changes "
"file." % field)
try:
format = float(self._dict["Format"])
except KeyError:
# If format is missing, pretend it's 1.5
format = 1.5
if format < 1.5 or format > 2.0:
raise UploadError(
"Format out of acceptable range for changes file. Range "
"1.5 - 2.0, format %g" % format)
def checkFileName(self):
"""Make sure the changes file name is well-formed.
Please note: for well-formed changes file names the `filename_archtag`
property will be set appropriately.
"""
match_changes = re_changes_file_name.match(self.filename)
if match_changes is None:
yield UploadError(
'%s -> inappropriate changesfile name, '
'should follow "<pkg>_<version>_<arch>.changes" format'
% self.filename)
else:
self.filename_archtag = match_changes.group(3)
def processAddresses(self):
"""Parse addresses and build person objects.
Process 'maintainer' and 'changed_by' addresses separately and return
an iterator over all exceptions generated while processing them.
"""
if self.signer:
# We only set the maintainer attribute up if we received a
# signed upload. This is desireable because it avoids us
# doing ensurePerson() for buildds and sync owners.
try:
self.maintainer = self.parseAddress(self._dict['Maintainer'])
except UploadError, error:
yield error
try:
self.changed_by = self.parseAddress(self._dict['Changed-By'])
except UploadError, error:
yield error
def isCustom(self, component_and_section):
"""Check if given 'component_and_section' matches a custom upload.
We recognize an upload as custom if it is targeted at a section like
'raw-<something>'.
Further checks will be performed in CustomUploadFile class.
"""
component_name, section_name = splitComponentAndSection(
component_and_section)
if section_name.startswith('raw-'):
return True
return False
def processFiles(self):
"""Build objects for each file mentioned in this changesfile.
This method is an error generator, i.e, it returns an iterator over
all exceptions that are generated while processing all mentioned
files.
"""
files = []
for fileline in self._dict['Files'].strip().split("\n"):
# files lines from a changes file are always of the form:
# CHECKSUM SIZE [COMPONENT/]SECTION PRIORITY FILENAME
digest, size, component_and_section, priority_name, filename = (
fileline.strip().split())
filepath = os.path.join(self.dirname, filename)
try:
if self.isCustom(component_and_section):
# This needs to be the first check, because
# otherwise the tarballs in custom uploads match
# with source_match.
file_instance = CustomUploadFile(
filepath, digest, size, component_and_section,
priority_name, self.policy, self.logger)
else:
try:
package, cls = determine_file_class_and_name(filename)
except CannotDetermineFileTypeError:
yield UploadError(
"Unable to identify file %s (%s) in changes."
% (filename, component_and_section))
continue
file_instance = cls(
filepath, digest, size, component_and_section,
priority_name, package, self.version, self,
self.policy, self.logger)
if cls == DSCFile:
self.dsc = file_instance
except UploadError, error:
yield error
else:
files.append(file_instance)
self.files = files
def verify(self):
"""Run all the verification checks on the changes data.
This method is an error generator, i.e, it returns an iterator over
all exceptions that are generated while verifying the changesfile
consistency.
"""
self.logger.debug("Verifying the changes file.")
if len(self.files) == 0:
yield UploadError("No files found in the changes")
if 'Urgency' not in self._dict:
# Urgency is recommended but not mandatory. Default to 'low'
self._dict['Urgency'] = "low"
raw_urgency = self._dict['Urgency'].lower()
if raw_urgency not in self.urgency_map:
yield UploadWarning(
"Unable to grok urgency %s, overriding with 'low'"
% (raw_urgency))
self._dict['Urgency'] = "low"
if not self.policy.unsigned_changes_ok:
assert self.signer is not None, (
"Policy does not allow unsigned changesfile")
#
# useful properties
#
@property
def filename(self):
"""Return the changesfile name."""
return os.path.basename(self.filepath)
@property
def dirname(self):
"""Return the current upload path name."""
return os.path.dirname(self.filepath)
def _getFilesByType(self, upload_filetype):
"""Look up for specific type of processed uploaded files.
It ensure the files mentioned in the changes are already processed.
"""
assert self.files is not None, "Files must but processed first."
return [upload_file for upload_file in self.files
if isinstance(upload_file, upload_filetype)]
@property
def binary_package_files(self):
"""Get a list of BaseBinaryUploadFile initialized in this context."""
return self._getFilesByType(BaseBinaryUploadFile)
@property
def source_package_files(self):
"""Return a list of SourceUploadFile initialized in this context."""
return self._getFilesByType(SourceUploadFile)
@property
def custom_files(self):
"""Return a list of CustomUploadFile initialized in this context."""
return self._getFilesByType(CustomUploadFile)
@property
def suite_name(self):
"""Returns the targeted suite name.
For example, 'hoary' or 'hoary-security'.
"""
return self._dict['Distribution']
@property
def architectures(self):
"""Return set of strings specifying architectures listed in file.
For instance ['source', 'all'] or ['source', 'i386', 'amd64']
or ['source'].
"""
return set(self._dict['Architecture'].split())
@property
def binaries(self):
"""Return set of binary package names listed."""
return set(self._dict['Binary'].strip().split())
@property
def converted_urgency(self):
"""Return the appropriate SourcePackageUrgency item."""
return self.urgency_map[self._dict['Urgency'].lower()]
@property
def version(self):
"""Return changesfile claimed version."""
return self._dict['Version']
@classmethod
def formatChangesComment(cls, comment):
"""A class utility method for formatting changes for display."""
# Return the display version of the comment using the
# debian policy rules. First replacing the blank line
# indicator '\n .' and then stripping one space from each
# successive line.
comment = comment.replace('\n .', '\n')
comment = comment.replace('\n ', '\n')
return comment
@property
def changes_comment(self):
"""Return changesfile 'change' comment."""
comment = self._dict['Changes']
return self.formatChangesComment(comment)
@property
def date(self):
"""Return changesfile date."""
return self._dict['Date']
@property
def source(self):
"""Return changesfile claimed source name."""
return self._dict['Source']
@property
def architecture_line(self):
"""Return changesfile archicteture line."""
return self._dict['Architecture']
@property
def simulated_changelog(self):
"""Build and return a changelog entry for this changesfile.
it includes the change comments followed by the author identification.
{{{
<CHANGES_COMMENT>
-- <CHANGED-BY> <DATE>
}}}
"""
changes_author = (
'\n -- %s %s' %
(self.changed_by['rfc822'], self.date))
return self.changes_comment + changes_author
def determine_file_class_and_name(filename):
"""Determine the name and PackageUploadFile subclass for the filename."""
source_match = re_issource.match(filename)
binary_match = re_isadeb.match(filename)
if source_match:
package = source_match.group(1)
if (determine_source_file_type(filename) ==
SourcePackageFileType.DSC):
cls = DSCFile
else:
cls = SourceUploadFile
elif binary_match:
package = binary_match.group(1)
cls = {
BinaryPackageFileType.DEB: DebBinaryUploadFile,
BinaryPackageFileType.DDEB: DdebBinaryUploadFile,
BinaryPackageFileType.UDEB: UdebBinaryUploadFile,
}[determine_binary_file_type(filename)]
else:
raise CannotDetermineFileTypeError(
"Could not determine the type of %r" % filename)
return package, cls
|