1
# Copyright 2010 Canonical Ltd. This software is licensed under the
1
# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
2
2
# GNU Affero General Public License version 3 (see the file LICENSE).
10
from zope.component import getUtility
12
from lazr.delegates import delegates
14
from storm.locals import (
11
20
from zope.interface import (
25
from canonical.database.enumcol import EnumCol
26
from canonical.launchpad.components.decoratedresultset import (
16
29
from canonical.launchpad.interfaces.lpstorm import (
33
from lp.app.errors import NotFoundError
20
34
from lp.registry.interfaces.pocket import PackagePublishingPocket
21
from lp.soyuz.interfaces.archive import (
25
from lp.soyuz.interfaces.distributionjob import (
35
from lp.registry.model.distroseries import DistroSeries
36
from lp.services.database.stormbase import StormBase
37
from lp.services.job.interfaces.job import JobStatus
38
from lp.services.job.model.job import Job
39
from lp.services.job.runner import BaseRunnableJob
40
from lp.soyuz.interfaces.archive import CannotCopy
41
from lp.soyuz.interfaces.packagecopyjob import (
28
IPackageCopyJobSource,
30
from lp.soyuz.model.distributionjob import (
32
DistributionJobDerived,
44
IPlainPackageCopyJobSource,
47
from lp.soyuz.model.archive import Archive
34
48
from lp.soyuz.scripts.packagecopier import do_copy
37
class PackageCopyJob(DistributionJobDerived):
38
"""Job that copies a package between archives."""
51
def specify_dsd_package(dsd):
52
"""Return (name, parent version) for `dsd`'s package.
54
This describes the package that `dsd` is for in a format suitable for
55
`PlainPackageCopyJobSource`.
57
:param dsd: A `DistroSeriesDifference`.
59
return (dsd.source_package_name.name, dsd.parent_source_version)
62
class PackageCopyJob(StormBase):
63
"""Base class for package copying jobs."""
40
65
implements(IPackageCopyJob)
42
class_job_type = DistributionJobType.COPY_PACKAGE
43
classProvides(IPackageCopyJobSource)
67
__storm_table__ = 'PackageCopyJob'
69
id = Int(primary=True)
71
job_id = Int(name='job')
72
job = Reference(job_id, Job.id)
74
source_archive_id = Int(name='source_archive')
75
source_archive = Reference(source_archive_id, Archive.id)
77
target_archive_id = Int(name='target_archive')
78
target_archive = Reference(target_archive_id, Archive.id)
80
target_distroseries_id = Int(name='target_distroseries')
81
target_distroseries = Reference(target_distroseries_id, DistroSeries.id)
83
job_type = EnumCol(enum=PackageCopyJobType, notNull=True)
85
_json_data = Unicode('json_data')
87
def __init__(self, source_archive, target_archive, target_distroseries,
89
super(PackageCopyJob, self).__init__()
91
self.source_archive = source_archive
92
self.target_archive = target_archive
93
self.target_distroseries = target_distroseries
94
self.job_type = job_type
95
self._json_data = self.serializeMetadata(metadata)
98
def serializeMetadata(cls, metadata_dict):
99
"""Serialize a dict of metadata into a unicode string."""
100
return simplejson.dumps(metadata_dict).decode('utf-8')
104
return simplejson.loads(self._json_data)
107
class PackageCopyJobDerived(BaseRunnableJob):
108
"""Abstract class for deriving from PackageCopyJob."""
110
delegates(IPackageCopyJob)
112
def __init__(self, job):
116
def get(cls, job_id):
119
:return: the PackageCopyJob with the specified id, as the current
120
PackageCopyJobDerived subclass.
121
:raises: NotFoundError if there is no job with the specified id, or
122
its job_type does not match the desired subclass.
124
job = PackageCopyJob.get(job_id)
125
if job.job_type != cls.class_job_type:
127
'No object found with id %d and type %s' % (job_id,
128
cls.class_job_type.title))
133
"""Iterate through all ready PackageCopyJobs."""
134
jobs = IStore(PackageCopyJob).find(
136
And(PackageCopyJob.job_type == cls.class_job_type,
137
PackageCopyJob.job == Job.id,
138
Job.id.is_in(Job.ready_jobs)))
139
return (cls(job) for job in jobs)
141
def getOopsVars(self):
142
"""See `IRunnableJob`."""
143
vars = super(PackageCopyJobDerived, self).getOopsVars()
145
('source_archive_id', self.context.source_archive_id),
146
('target_archive_id', self.context.target_archive_id),
147
('target_distroseries_id', self.context.target_distroseries_id),
148
('package_copy_job_id', self.context.id),
149
('package_copy_job_type', self.context.job_type.title),
154
class PlainPackageCopyJob(PackageCopyJobDerived):
155
"""Job that copies packages between archives."""
157
implements(IPlainPackageCopyJob)
159
class_job_type = PackageCopyJobType.PLAIN
160
classProvides(IPlainPackageCopyJobSource)
46
163
def create(cls, source_packages, source_archive,
47
164
target_archive, target_distroseries, target_pocket,
48
165
include_binaries=False):
49
"""See `IPackageCopyJobSource`."""
166
"""See `IPlainPackageCopyJobSource`."""
51
168
'source_packages': source_packages,
52
'source_archive_id': source_archive.id,
53
'target_archive_id': target_archive.id,
54
169
'target_pocket': target_pocket.value,
55
'include_binaries': include_binaries,
170
'include_binaries': bool(include_binaries),
57
job = DistributionJob(
58
target_distroseries.distribution, target_distroseries,
59
cls.class_job_type, metadata)
60
IMasterStore(DistributionJob).add(job)
172
job = PackageCopyJob(
173
source_archive=source_archive,
174
target_archive=target_archive,
175
target_distroseries=target_distroseries,
176
job_type=cls.class_job_type,
178
IMasterStore(PackageCopyJob).add(job)
64
def getActiveJobs(cls, archive):
65
"""See `IPackageCopyJobSource`."""
66
# TODO: JRV 20101104. This iterates manually over all active
67
# PackageCopyJobs. This should usually be a short enough list,
68
# but if it really becomes an issue target_archive should
69
# be moved into a separate database field.
70
jobs = IStore(DistributionJob).find(
72
DistributionJob.job_type == cls.class_job_type,
73
DistributionJob.distribution == archive.distribution)
74
jobs = [cls(job) for job in jobs]
75
return (job for job in jobs if job.target_archive_id == archive.id)
182
def getActiveJobs(cls, target_archive):
183
"""See `IPlainPackageCopyJobSource`."""
184
jobs = IStore(PackageCopyJob).find(
186
PackageCopyJob.job_type == cls.class_job_type,
187
PackageCopyJob.target_archive == target_archive,
188
Job.id == PackageCopyJob.job_id,
189
Job._status == JobStatus.WAITING)
190
jobs = jobs.order_by(PackageCopyJob.id)
191
return DecoratedResultSet(jobs, cls)
194
def getPendingJobsForTargetSeries(cls, target_series):
195
"""Get upcoming jobs for `target_series`, ordered by age."""
196
raw_jobs = IStore(PackageCopyJob).find(
198
Job.id == PackageCopyJob.job_id,
199
PackageCopyJob.job_type == cls.class_job_type,
200
PackageCopyJob.target_distroseries == target_series,
201
Job._status.is_in(Job.PENDING_STATUSES))
202
raw_jobs = raw_jobs.order_by(PackageCopyJob.id)
203
return DecoratedResultSet(raw_jobs, cls)
206
def getPendingJobsPerPackage(cls, target_series):
207
"""See `IPlainPackageCopyJobSource`."""
209
# Go through jobs in-order, picking the first matching job for
210
# any (package, version) tuple. Because of how
211
# getPendingJobsForTargetSeries orders its results, the first
212
# will be the oldest and thus presumably the first to finish.
213
for job in cls.getPendingJobsForTargetSeries(target_series):
214
for package in job.metadata["source_packages"]:
215
result.setdefault(tuple(package), job)
78
219
def source_packages(self):