1
# Copyright 2009 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
"""Code for 'processing' 'uploads'. Also see nascentupload.py.
6
Uploads are directories in the 'incoming' queue directory. They may have
7
arrived manually from a distribution contributor, via a poppy upload, or
8
they may have come from a build.
10
Within an upload, we may find no changes file, one, or several. One is
11
the usual number. To process the upload, we process each changes file
12
in turn. These changes files may be within a structure of sub-directories,
13
in which case we extract information from the names of these, to calculate
14
which distribution and which PPA are being uploaded to.
16
To process a changes file, we make checks such as that the other files
17
referenced by it are present, formatting is valid, signatures are correct,
18
checksums match, and that the .changes file represents an upload which makes
19
sense, eg. it is not a binary for which we have no source, or an older
20
version than already exists in the same target distroseries pocket.
22
Depending on the outcome of these checks, the changes file will either be
23
accepted (and the information from it, and the referenced files, imported
24
into the database) or it won't (and the database will be unchanged). If not
25
accepted, a changes file might be 'failed' or 'rejected', where failed
26
changes files are dropped silently, but rejected ones generate a rejection
27
email back to the uploader.
29
There are several valid reasons to fail (the changes file is so mangled
30
that we can't read who we should send a rejection to, or it's not correctly
31
signed, so we can't be sure a rejection wouldn't be spam (it may not have
32
been uploaded by who it says it was uploaded by). In practice, in the code
33
as it stands, we also consider the processing of a changes file to have
34
failed if it generates an unexpected exception, and there are some known
35
cases where it does this and a rejection would have been more useful
38
Each upload directory is saved after processing, in case it is needed for
39
debugging purposes. This is done by moving it to a directory inside the queue
40
directory, beside incoming, named after the result - 'failed', 'rejected' or
41
'accepted'. Where there are no changes files, the upload is considered failed,
42
and where there is more than one changes file, the upload is assigned the
43
worst of the results from the various changes files found (in the order
44
above, failed being worst).
55
from contrib.glock import GlobalLock
56
from sqlobject import SQLObjectNotFound
57
from zope.component import getUtility
59
from canonical.launchpad.webapp.errorlog import (
60
ErrorReportingUtility,
63
from lp.app.errors import NotFoundError
64
from lp.archiveuploader.nascentupload import (
65
EarlyReturnUploadError,
69
from lp.archiveuploader.uploadpolicy import (
70
BuildDaemonUploadPolicy,
73
from lp.buildmaster.enums import BuildStatus
74
from lp.buildmaster.interfaces.buildfarmjob import ISpecificBuildFarmJobSource
75
from lp.code.interfaces.sourcepackagerecipebuild import (
76
ISourcePackageRecipeBuild,
78
from lp.registry.interfaces.distribution import IDistributionSet
79
from lp.registry.interfaces.person import IPersonSet
80
from lp.services.log.logger import BufferLogger
81
from lp.soyuz.interfaces.archive import (
89
'parse_build_upload_leaf_name',
93
UPLOAD_PATH_ERROR_TEMPLATE = (
94
"""Launchpad failed to process the upload path '%(upload_path)s':
98
It is likely that you have a configuration problem with dput/dupload.
103
def parse_build_upload_leaf_name(name):
104
"""Parse the leaf directory name of a build upload.
106
:param name: Directory name.
107
:return: Tuple with build farm job id.
109
(job_type, job_id_str) = name.split("-")[-2:]
111
return (job_type, int(job_id_str))
116
class UploadStatusEnum:
117
"""Possible results from processing an upload.
119
ACCEPTED: all goes well, we commit nascentupload's changes to the db
120
REJECTED: nascentupload gives a well-formed rejection error,
121
we send a rejection email and rollback.
122
FAILED: nascentupload code raises an exception, no email, rollback
124
ACCEPTED = 'accepted'
125
REJECTED = 'rejected'
129
class UploadPathError(Exception):
130
"""This exception happened when parsing the upload path."""
133
class PPAUploadPathError(Exception):
134
"""Exception when parsing a PPA upload path."""
137
class UploadProcessor:
138
"""Responsible for processing uploads. See module docstring."""
140
def __init__(self, base_fsroot, dry_run, no_mails, builds, keep,
141
policy_for_distro, ztm, log):
142
"""Create a new upload processor.
144
:param base_fsroot: Root path for queue to use
145
:param dry_run: Run but don't commit changes to database
146
:param no_mails: Don't send out any emails
147
:param builds: Interpret leaf names as build ids
148
:param keep: Leave the files in place, don't move them away
149
:param policy_for_distro: callback to obtain Policy object for a
151
:param ztm: Database transaction to use
152
:param log: Logger to use for reporting
154
self.base_fsroot = base_fsroot
155
self.dry_run = dry_run
157
self.last_processed_upload = None
159
self.no_mails = no_mails
161
self._getPolicyForDistro = policy_for_distro
164
def processUploadQueue(self, leaf_name=None):
165
"""Search for uploads, and process them.
167
Uploads are searched for in the 'incoming' directory inside the
170
This method also creates the 'incoming', 'accepted', 'rejected', and
171
'failed' directories inside the base_fsroot if they don't yet exist.
174
self.log.debug("Beginning processing")
176
for subdir in ["incoming", "accepted", "rejected", "failed"]:
177
full_subdir = os.path.join(self.base_fsroot, subdir)
178
if not os.path.exists(full_subdir):
179
self.log.debug("Creating directory %s" % full_subdir)
180
os.mkdir(full_subdir)
182
fsroot = os.path.join(self.base_fsroot, "incoming")
183
uploads_to_process = self.locateDirectories(fsroot)
184
self.log.debug("Checked in %s, found %s"
185
% (fsroot, uploads_to_process))
186
for upload in uploads_to_process:
187
self.log.debug("Considering upload %s" % upload)
188
if leaf_name is not None and upload != leaf_name:
189
self.log.debug("Skipping %s -- does not match %s" % (
193
handler = UploadHandler.forProcessor(self, fsroot, upload)
194
except CannotGetBuild, e:
199
self.log.debug("Rolling back any remaining transactions.")
202
def locateDirectories(self, fsroot):
203
"""Return a list of upload directories in a given queue.
205
This method operates on the queue atomically, i.e. it suppresses
206
changes in the queue directory, like new uploads, by acquiring
207
the shared upload_queue lockfile while the directory are listed.
209
:param fsroot: path to a 'queue' directory to be inspected.
211
:return: a list of upload directories found in the queue
212
alphabetically sorted.
214
# Protecting listdir by a lock ensures that we only get completely
215
# finished directories listed. See lp.poppy.hooks for the other
217
lockfile_path = os.path.join(fsroot, ".lock")
218
fsroot_lock = GlobalLock(lockfile_path)
219
mode = stat.S_IMODE(os.stat(lockfile_path).st_mode)
221
# XXX cprov 20081024 bug=185731: The lockfile permission can only be
222
# changed by its owner. Since we can't predict which process will
223
# create it in production systems we simply ignore errors when trying
224
# to grant the right permission. At least, one of the process will
227
os.chmod(lockfile_path, mode | stat.S_IWGRP)
229
self.log.debug('Could not fix the lockfile permission: %s' % err)
232
fsroot_lock.acquire(blocking=True)
233
dir_names = os.listdir(fsroot)
235
# Skip lockfile deletion, see similar code in lp.poppy.hooks.
236
fsroot_lock.release(skip_delete=True)
238
sorted_dir_names = sorted(
240
for dir_name in dir_names
241
if os.path.isdir(os.path.join(fsroot, dir_name)))
243
return sorted_dir_names
247
"""Handler for processing a single upload."""
249
def __init__(self, processor, fsroot, upload):
252
:param processor: The `UploadProcessor` that requested processing the
254
:param fsroot: Path to the directory containing the upload directory
255
:param upload: Name of the directory containing the upload.
257
self.processor = processor
260
self.upload_path = os.path.join(self.fsroot, self.upload)
263
def forProcessor(processor, fsroot, upload, build=None):
264
"""Instantiate an UploadHandler subclass for a given upload.
266
:param processor: The `UploadProcessor` that requested processing the
268
:param fsroot: Path to the directory containing the upload directory
269
:param upload: Name of the directory containing the upload.
270
:param build: Optional; the build that produced the upload.
273
# Upload directories contain build results,
274
# directories are named after job ids.
275
return BuildUploadHandler(processor, fsroot, upload, build)
278
return UserUploadHandler(processor, fsroot, upload)
280
def locateChangesFiles(self):
281
"""Locate .changes files in the upload directory.
283
Return .changes files sorted with *_source.changes first. This
284
is important to us, as in an upload containing several changes files,
285
it's possible the binary ones will depend on the source ones, so
286
the source ones should always be considered first.
290
for dirpath, dirnames, filenames in os.walk(self.upload_path):
291
relative_path = dirpath[len(self.upload_path) + 1:]
292
for filename in filenames:
293
if filename.endswith(".changes"):
294
changes_files.append(
295
os.path.join(relative_path, filename))
296
return self.orderFilenames(changes_files)
298
def processChangesFile(self, changes_file, logger=None):
299
"""Process a single changes file.
301
This is done by obtaining the appropriate upload policy (according
302
to command-line options and the value in the .distro file beside
303
the upload, if present), creating a NascentUpload object and calling
306
We obtain the context for this processing from the relative path,
307
within the upload folder, of this changes file. This influences
308
our creation both of upload policy and the NascentUpload object.
310
See nascentupload.py for the gory details.
312
Returns a value from UploadStatusEnum, or re-raises an exception
315
:param changes_file: filename of the changes file to process.
316
:param logger: logger to use for processing.
317
:return: an `UploadStatusEnum` value
320
logger = self.processor.log
321
# Calculate the distribution from the path within the upload
322
# Reject the upload since we could not process the path,
323
# Store the exception information as a rejection message.
324
relative_path = os.path.dirname(changes_file)
325
upload_path_error = None
327
(distribution, suite_name,
328
archive) = parse_upload_path(relative_path)
329
except UploadPathError, e:
330
# pick some defaults to create the NascentUpload() object.
331
# We will be rejecting the upload so it doesn matter much.
332
distribution = getUtility(IDistributionSet)['ubuntu']
334
archive = distribution.main_archive
335
upload_path_error = UPLOAD_PATH_ERROR_TEMPLATE % (
336
dict(upload_path=relative_path, path_error=str(e),
338
"Please update your dput/dupload configuration "
339
"and then re-upload.")))
340
except PPAUploadPathError, e:
341
# Again, pick some defaults but leave a hint for the rejection
342
# emailer that it was a PPA failure.
343
distribution = getUtility(IDistributionSet)['ubuntu']
345
# XXX cprov 20071212: using the first available PPA is not exactly
346
# fine because it can confuse the code that sends rejection
347
# messages if it relies only on archive.purpose (which should be
348
# enough). On the other hand if we set an arbitrary owner it
349
# will break nascentupload ACL calculations.
350
archive = distribution.getAllPPAs()[0]
351
upload_path_error = UPLOAD_PATH_ERROR_TEMPLATE % (
352
dict(upload_path=relative_path, path_error=str(e),
354
"Please check the documentation at "
355
"https://help.launchpad.net/Packaging/PPA#Uploading "
356
"and update your configuration.")))
357
logger.debug("Finding fresh policy")
358
policy = self._getPolicyForDistro(distribution)
359
policy.archive = archive
361
# DistroSeries overriding respect the following precedence:
362
# 1. process-upload.py command-line option (-r),
364
# 3. changesfile 'Distribution' field.
365
if suite_name is not None:
366
policy.setDistroSeriesAndPocket(suite_name)
368
# The path we want for NascentUpload is the path to the folder
369
# containing the changes file (and the other files referenced by it).
370
changesfile_path = os.path.join(self.upload_path, changes_file)
372
upload = NascentUpload.from_changesfile_path(
373
changesfile_path, policy, self.processor.log)
374
except UploadError as e:
375
# We failed to parse the changes file, so we have no key or
376
# Changed-By to notify of the rejection. Just log it and
378
# XXX wgrant 2011-09-29 bug=499438: With some refactoring we
379
# could do better here: if we have a signature then we have
380
# somebody to email, even if the rest of the file is
383
"Failed to parse changes file '%s': %s" % (
384
os.path.join(self.upload_path, changes_file),
386
return UploadStatusEnum.REJECTED
388
# Reject source upload to buildd upload paths.
389
first_path = relative_path.split(os.path.sep)[0]
390
if (first_path.isdigit() and
391
policy.name != BuildDaemonUploadPolicy.name):
393
"Invalid upload path (%s) for this policy (%s)" %
394
(relative_path, policy.name))
395
upload.reject(error_message)
396
logger.error(error_message)
398
# Reject upload with path processing errors.
399
if upload_path_error is not None:
400
upload.reject(upload_path_error)
402
# Store processed NascentUpload instance, mostly used for tests.
403
self.processor.last_processed_upload = upload
406
logger.info("Processing upload %s" % upload.changes.filename)
407
result = UploadStatusEnum.ACCEPTED
410
self._processUpload(upload)
411
except UploadPolicyError, e:
412
upload.reject("UploadPolicyError escaped upload.process: "
415
"UploadPolicyError escaped upload.process", exc_info=True)
416
except (KeyboardInterrupt, SystemExit):
418
except EarlyReturnUploadError:
419
# An error occurred that prevented further error collection,
420
# add this fact to the list of errors.
422
"Further error processing not possible because of "
423
"a critical previous error.")
425
# In case of unexpected unhandled exception, we'll
426
# *try* to reject the upload. This may fail and cause
427
# a further exception, depending on the state of the
428
# nascentupload objects. In that case, we've lost nothing,
429
# the new exception will be handled by the caller just like
430
# the one we caught would have been, by failing the upload
432
logger.exception("Unhandled exception processing upload")
433
upload.reject("Unhandled exception processing upload: %s" % e)
435
# XXX julian 2007-05-25 bug=29744:
436
# When bug #29744 is fixed (zopeless mails should only be sent
437
# when transaction is committed) this will cause any emails sent
438
# sent by do_reject to be lost.
440
if self.processor.dry_run or self.processor.no_mails:
442
if upload.is_rejected:
443
result = UploadStatusEnum.REJECTED
444
upload.do_reject(notify)
445
self.processor.ztm.abort()
447
successful = self._acceptUpload(upload, notify)
449
result = UploadStatusEnum.REJECTED
451
"Rejection during accept. Aborting partial accept.")
452
self.processor.ztm.abort()
454
if upload.is_rejected:
455
logger.info("Upload was rejected:")
456
for msg in upload.rejections:
457
logger.info("\t%s" % msg)
459
if self.processor.dry_run:
460
logger.info("Dry run, aborting transaction.")
461
self.processor.ztm.abort()
464
"Committing the transaction and any mails associated "
466
self.processor.ztm.commit()
468
self.processor.ztm.abort()
473
def removeUpload(self, logger):
474
"""Remove an upload that has succesfully been processed.
476
This includes moving the given upload directory and moving the
477
matching .distro file, if it exists.
479
:param logger: The logger to use for logging results.
481
if self.processor.keep or self.processor.dry_run:
482
logger.debug("Keeping contents untouched")
485
logger.debug("Removing upload directory %s", self.upload_path)
486
shutil.rmtree(self.upload_path)
488
distro_filename = self.upload_path + ".distro"
489
if os.path.isfile(distro_filename):
490
logger.debug("Removing distro file %s", distro_filename)
491
os.remove(distro_filename)
493
def moveProcessedUpload(self, destination, logger):
494
"""Move or remove the upload depending on the status of the upload.
496
:param destination: An `UploadStatusEnum` value.
497
:param logger: The logger to use for logging results.
499
if destination == "accepted":
500
self.removeUpload(logger)
502
self.moveUpload(destination, logger)
504
def moveUpload(self, subdir_name, logger):
505
"""Move the upload to the named subdir of the root, eg 'accepted'.
507
This includes moving the given upload directory and moving the
508
matching .distro file, if it exists.
510
:param subdir_name: Name of the subdirectory to move to.
511
:param logger: The logger to use for logging results.
513
if self.processor.keep or self.processor.dry_run:
514
logger.debug("Keeping contents untouched")
517
pathname = os.path.basename(self.upload_path)
519
target_path = os.path.join(
520
self.processor.base_fsroot, subdir_name, pathname)
521
logger.debug("Moving upload directory %s to %s" %
522
(self.upload_path, target_path))
523
shutil.move(self.upload_path, target_path)
525
distro_filename = self.upload_path + ".distro"
526
if os.path.isfile(distro_filename):
527
target_path = os.path.join(self.processor.base_fsroot,
529
os.path.basename(distro_filename))
530
logger.debug("Moving distro file %s to %s" % (distro_filename,
532
shutil.move(distro_filename, target_path)
535
def orderFilenames(fnames):
536
"""Order filenames, sorting *_source.changes before others.
538
Aside from that, a standard string sort.
541
def sourceFirst(filename):
542
return (not filename.endswith("_source.changes"), filename)
544
return sorted(fnames, key=sourceFirst)
547
class UserUploadHandler(UploadHandler):
550
"""Process an upload's changes files, and move it to a new directory.
552
The destination directory depends on the result of the processing
553
of the changes files. If there are no changes files, the result
554
is 'failed', otherwise it is the worst of the results from the
555
individual changes files, in order 'failed', 'rejected', 'accepted'.
557
changes_files = self.locateChangesFiles()
561
for changes_file in changes_files:
562
self.processor.log.debug(
563
"Considering changefile %s" % changes_file)
565
results.add(self.processChangesFile(
566
changes_file, self.processor.log))
567
except (KeyboardInterrupt, SystemExit):
570
info = sys.exc_info()
572
'Exception while processing upload %s' % self.upload_path)
573
properties = [('error-explanation', message)]
574
request = ScriptRequest(properties)
575
error_utility = ErrorReportingUtility()
576
error_utility.raising(info, request)
577
self.processor.log.error(
578
'%s (%s)' % (message, request.oopsid))
579
results.add(UploadStatusEnum.FAILED)
580
if len(results) == 0:
581
destination = UploadStatusEnum.FAILED
584
UploadStatusEnum.FAILED, UploadStatusEnum.REJECTED,
585
UploadStatusEnum.ACCEPTED]:
586
if destination in results:
588
self.moveProcessedUpload(destination, self.processor.log)
590
def _getPolicyForDistro(self, distribution):
591
return self.processor._getPolicyForDistro(distribution, None)
593
def _processUpload(self, upload):
596
def _acceptUpload(self, upload, notify):
597
return upload.do_accept(notify=notify, build=None)
600
class CannotGetBuild(Exception):
602
"""Attempting to retrieve the build for this upload failed."""
605
class BuildUploadHandler(UploadHandler):
607
def __init__(self, processor, fsroot, upload, build=None):
611
:build: Optional build that produced this upload. If not supplied,
612
will be retrieved using the id in the upload.
613
:raises: CannotGetBuild if the build could not be retrieved.
615
super(BuildUploadHandler, self).__init__(processor, fsroot, upload)
617
if self.build is None:
618
self.build = self._getBuild()
620
def _getPolicyForDistro(self, distribution):
621
return self.processor._getPolicyForDistro(distribution, self.build)
623
def _processUpload(self, upload):
624
upload.process(self.build)
626
def _acceptUpload(self, upload, notify):
627
return upload.do_accept(notify=notify, build=self.build)
631
job_type, job_id = parse_build_upload_leaf_name(self.upload)
633
raise CannotGetBuild(
634
"Unable to extract build id from leaf name %s, skipping." %
637
return getUtility(ISpecificBuildFarmJobSource, job_type).getByID(
639
except NotFoundError:
640
raise CannotGetBuild(
641
"Unable to find %s with id %d. Skipping." %
645
"""Process an upload that is the result of a build.
647
The name of the leaf is the build id of the build.
648
Build uploads always contain a single package per leaf.
650
logger = BufferLogger()
651
if self.build.status != BuildStatus.UPLOADING:
652
self.processor.log.warn(
653
"Expected build status to be 'UPLOADING', was %s. Ignoring." %
654
self.build.status.name)
657
# The recipe may have been deleted so we need to flag that here
658
# and will handle below. We check so that we don't go to the
659
# expense of doing an unnecessary upload. We don't just exit here
660
# because we want the standard cleanup to occur.
661
recipe_deleted = (ISourcePackageRecipeBuild.providedBy(self.build)
662
and self.build.recipe is None)
664
result = UploadStatusEnum.FAILED
666
self.processor.log.debug("Build %s found" % self.build.id)
667
[changes_file] = self.locateChangesFiles()
668
logger.debug("Considering changefile %s" % changes_file)
669
result = self.processChangesFile(changes_file, logger)
670
except (KeyboardInterrupt, SystemExit):
673
info = sys.exc_info()
675
'Exception while processing upload %s' % self.upload_path)
676
properties = [('error-explanation', message)]
677
request = ScriptRequest(properties)
678
error_utility = ErrorReportingUtility()
679
error_utility.raising(info, request)
680
logger.error('%s (%s)' % (message, request.oopsid))
681
result = UploadStatusEnum.FAILED
682
if (result != UploadStatusEnum.ACCEPTED or
683
not self.build.verifySuccessfulUpload()):
684
self.build.status = BuildStatus.FAILEDTOUPLOAD
685
if self.build.status != BuildStatus.FULLYBUILT:
687
# For a deleted recipe, no need to notify that uploading has
688
# failed - we just log a warning.
689
self.processor.log.warn(
690
"Recipe for build %s was deleted. Ignoring." %
693
self.build.storeUploadLog(logger.getLogBuffer())
694
self.build.notify(extra_info="Uploading build %s failed." %
698
self.processor.ztm.commit()
699
self.moveProcessedUpload(result, logger)
702
def _getDistributionAndSuite(parts, exc_type):
703
"""Return an `IDistribution` and a valid suite name for the given path.
706
Helper function used within `parse_upload_path` for extracting and
707
verifying the part of the upload path targeting a existing distribution
708
and optionally one of its suite.
710
It will fail with `AssertionError` if the given `parts` is not a list
711
with one or two elements.
713
:param parts: a list of path parts to be processed.
714
:param exc_type: a specific Exception type that should be raised on
717
:return: a tuple containing a `IDistribution` and a suite name if it's
718
appropriate. The suite name will be None if it wasn't present in the
721
:raises: the given `exc_type` if the corresponding distribution or suite
724
# This assertion should never happens when this method is called from
725
# 'parse_upload_path'.
726
assert len(parts) <= 2, (
727
"'%s' does not correspond to a [distribution[/suite]]."
730
# Uploads with undefined distribution defaults to 'ubuntu'.
731
if len(parts) == 0 or parts[0] is '':
732
ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
733
return (ubuntu, None)
735
distribution_name = parts[0]
736
distribution = getUtility(IDistributionSet).getByName(distribution_name)
737
if distribution is None:
739
"Could not find distribution '%s'." % distribution_name)
742
return (distribution, None)
744
suite_name = parts[1]
746
distribution.getDistroSeriesAndPocket(suite_name)
747
except NotFoundError:
748
raise exc_type("Could not find suite '%s'." % suite_name)
750
return (distribution, suite_name)
753
def parse_upload_path(relative_path):
754
"""Locate the distribution and archive for the upload.
756
We do this by analysing the path to which the user has uploaded,
757
ie. the relative path within the upload folder to the changes file.
760
'' - default distro, ubuntu
761
'<distroname>' - given distribution
762
'~<personname>[/ppa_name]/<distroname>[/distroseriesname]' - given ppa,
763
distribution and optionally a distroseries. If ppa_name is not
764
specified it will default to the one referenced by IPerson.archive.
766
I raises UploadPathError if something was wrong when parsing it.
768
On success it returns a tuple of IDistribution, suite-name,
769
IArchive for the given path, where the second field can be None.
771
parts = relative_path.split(os.path.sep)
773
first_path = parts[0]
775
if (not first_path.startswith('~') and not first_path.isdigit()
776
and len(parts) <= 2):
777
# Distribution upload (<distro>[/distroseries]). Always targeted to
778
# the corresponding primary archive.
779
distribution, suite_name = _getDistributionAndSuite(
780
parts, UploadPathError)
781
archive = distribution.main_archive
783
elif (first_path.startswith('~') and
786
# PPA upload (~<person>[/ppa_name]/<distro>[/distroseries]).
788
# Skip over '~' from the person name.
789
person_name = first_path[1:]
790
person = getUtility(IPersonSet).getByName(person_name)
792
raise PPAUploadPathError(
793
"Could not find person or team named '%s'." % person_name)
797
# Compatibilty feature for allowing unamed-PPA upload paths
798
# for a certain period of time.
799
if ppa_name == 'ubuntu':
801
distribution_and_suite = parts[1:]
803
distribution_and_suite = parts[2:]
806
archive = person.getPPAByName(ppa_name)
808
raise PPAUploadPathError(
809
"Could not find a PPA named '%s' for '%s'."
810
% (ppa_name, person_name))
812
distribution, suite_name = _getDistributionAndSuite(
813
distribution_and_suite, PPAUploadPathError)
815
elif first_path.isdigit():
816
# This must be a binary upload from a build slave.
818
archive = getUtility(IArchiveSet).get(int(first_path))
819
except SQLObjectNotFound:
820
raise UploadPathError(
821
"Could not find archive with id=%s." % first_path)
822
distribution, suite_name = _getDistributionAndSuite(
823
parts[1:], UploadPathError)
825
# Upload path does not match anything we support.
826
raise UploadPathError("Path format mismatch.")
828
if not archive.enabled:
829
raise PPAUploadPathError("%s is disabled." % archive.displayname)
831
if archive.distribution != distribution:
832
raise PPAUploadPathError(
833
"%s only supports uploads to '%s' distribution."
834
% (archive.displayname, archive.distribution.name))
836
return (distribution, suite_name, archive)