~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/lp/soyuz/model/packagecopyjob.py

Merge db-devel.

Show diffs side-by-side

added added

removed removed

Lines of Context:
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).
3
3
 
4
4
__metaclass__ = type
5
5
 
6
6
__all__ = [
7
7
    "PackageCopyJob",
 
8
    "PlainPackageCopyJob",
 
9
    "specify_dsd_package",
8
10
]
9
11
 
10
 
from zope.component import getUtility
 
12
from lazr.delegates import delegates
 
13
import simplejson
 
14
from storm.locals import (
 
15
    And,
 
16
    Int,
 
17
    Reference,
 
18
    Unicode,
 
19
    )
11
20
from zope.interface import (
12
21
    classProvides,
13
22
    implements,
14
23
    )
15
24
 
 
25
from canonical.database.enumcol import EnumCol
 
26
from canonical.launchpad.components.decoratedresultset import (
 
27
    DecoratedResultSet,
 
28
    )
16
29
from canonical.launchpad.interfaces.lpstorm import (
17
30
    IMasterStore,
18
31
    IStore,
19
32
    )
 
33
from lp.app.errors import NotFoundError
20
34
from lp.registry.interfaces.pocket import PackagePublishingPocket
21
 
from lp.soyuz.interfaces.archive import (
22
 
    CannotCopy,
23
 
    IArchiveSet,
24
 
    )
25
 
from lp.soyuz.interfaces.distributionjob import (
26
 
    DistributionJobType,
 
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 (
27
42
    IPackageCopyJob,
28
 
    IPackageCopyJobSource,
29
 
    )
30
 
from lp.soyuz.model.distributionjob import (
31
 
    DistributionJob,
32
 
    DistributionJobDerived,
33
 
    )
 
43
    IPlainPackageCopyJob,
 
44
    IPlainPackageCopyJobSource,
 
45
    PackageCopyJobType,
 
46
    )
 
47
from lp.soyuz.model.archive import Archive
34
48
from lp.soyuz.scripts.packagecopier import do_copy
35
49
 
36
50
 
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.
 
53
 
 
54
    This describes the package that `dsd` is for in a format suitable for
 
55
    `PlainPackageCopyJobSource`.
 
56
 
 
57
    :param dsd: A `DistroSeriesDifference`.
 
58
    """
 
59
    return (dsd.source_package_name.name, dsd.parent_source_version)
 
60
 
 
61
 
 
62
class PackageCopyJob(StormBase):
 
63
    """Base class for package copying jobs."""
39
64
 
40
65
    implements(IPackageCopyJob)
41
66
 
42
 
    class_job_type = DistributionJobType.COPY_PACKAGE
43
 
    classProvides(IPackageCopyJobSource)
 
67
    __storm_table__ = 'PackageCopyJob'
 
68
 
 
69
    id = Int(primary=True)
 
70
 
 
71
    job_id = Int(name='job')
 
72
    job = Reference(job_id, Job.id)
 
73
 
 
74
    source_archive_id = Int(name='source_archive')
 
75
    source_archive = Reference(source_archive_id, Archive.id)
 
76
 
 
77
    target_archive_id = Int(name='target_archive')
 
78
    target_archive = Reference(target_archive_id, Archive.id)
 
79
 
 
80
    target_distroseries_id = Int(name='target_distroseries')
 
81
    target_distroseries = Reference(target_distroseries_id, DistroSeries.id)
 
82
 
 
83
    job_type = EnumCol(enum=PackageCopyJobType, notNull=True)
 
84
 
 
85
    _json_data = Unicode('json_data')
 
86
 
 
87
    def __init__(self, source_archive, target_archive, target_distroseries,
 
88
                 job_type, metadata):
 
89
        super(PackageCopyJob, self).__init__()
 
90
        self.job = Job()
 
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)
 
96
 
 
97
    @classmethod
 
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')
 
101
 
 
102
    @property
 
103
    def metadata(self):
 
104
        return simplejson.loads(self._json_data)
 
105
 
 
106
 
 
107
class PackageCopyJobDerived(BaseRunnableJob):
 
108
    """Abstract class for deriving from PackageCopyJob."""
 
109
 
 
110
    delegates(IPackageCopyJob)
 
111
 
 
112
    def __init__(self, job):
 
113
        self.context = job
 
114
 
 
115
    @classmethod
 
116
    def get(cls, job_id):
 
117
        """Get a job by id.
 
118
 
 
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.
 
123
        """
 
124
        job = PackageCopyJob.get(job_id)
 
125
        if job.job_type != cls.class_job_type:
 
126
            raise NotFoundError(
 
127
                'No object found with id %d and type %s' % (job_id,
 
128
                cls.class_job_type.title))
 
129
        return cls(job)
 
130
 
 
131
    @classmethod
 
132
    def iterReady(cls):
 
133
        """Iterate through all ready PackageCopyJobs."""
 
134
        jobs = IStore(PackageCopyJob).find(
 
135
            PackageCopyJob,
 
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)
 
140
 
 
141
    def getOopsVars(self):
 
142
        """See `IRunnableJob`."""
 
143
        vars = super(PackageCopyJobDerived, self).getOopsVars()
 
144
        vars.extend([
 
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),
 
150
            ])
 
151
        return vars
 
152
 
 
153
 
 
154
class PlainPackageCopyJob(PackageCopyJobDerived):
 
155
    """Job that copies packages between archives."""
 
156
 
 
157
    implements(IPlainPackageCopyJob)
 
158
 
 
159
    class_job_type = PackageCopyJobType.PLAIN
 
160
    classProvides(IPlainPackageCopyJobSource)
44
161
 
45
162
    @classmethod
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`."""
50
167
        metadata = {
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),
56
171
            }
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,
 
177
            metadata=metadata)
 
178
        IMasterStore(PackageCopyJob).add(job)
61
179
        return cls(job)
62
180
 
63
181
    @classmethod
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(
71
 
            DistributionJob,
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(
 
185
            PackageCopyJob,
 
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)
 
192
 
 
193
    @classmethod
 
194
    def getPendingJobsForTargetSeries(cls, target_series):
 
195
        """Get upcoming jobs for `target_series`, ordered by age."""
 
196
        raw_jobs = IStore(PackageCopyJob).find(
 
197
            PackageCopyJob,
 
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)
 
204
 
 
205
    @classmethod
 
206
    def getPendingJobsPerPackage(cls, target_series):
 
207
        """See `IPlainPackageCopyJobSource`."""
 
208
        result = {}
 
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)
 
216
        return result
76
217
 
77
218
    @property
78
219
    def source_packages(self):
82
223
                name=name, version=version, exact_match=True).first()
83
224
 
84
225
    @property
85
 
    def source_archive_id(self):
86
 
        return self.metadata['source_archive_id']
87
 
 
88
 
    @property
89
 
    def source_archive(self):
90
 
        return getUtility(IArchiveSet).get(self.source_archive_id)
91
 
 
92
 
    @property
93
 
    def target_archive_id(self):
94
 
        return self.metadata['target_archive_id']
95
 
 
96
 
    @property
97
 
    def target_archive(self):
98
 
        return getUtility(IArchiveSet).get(self.target_archive_id)
99
 
 
100
 
    @property
101
 
    def target_distroseries(self):
102
 
        return self.distroseries
103
 
 
104
 
    @property
105
226
    def target_pocket(self):
106
227
        return PackagePublishingPocket.items[self.metadata['target_pocket']]
107
228
 
128
249
            sources=source_packages, archive=self.target_archive,
129
250
            series=self.target_distroseries, pocket=self.target_pocket,
130
251
            include_binaries=self.include_binaries, check_permissions=False)
 
252
 
 
253
    def __repr__(self):
 
254
        """Returns an informative representation of the job."""
 
255
        parts = ["%s to copy" % self.__class__.__name__]
 
256
        source_packages = self.metadata["source_packages"]
 
257
        if len(source_packages) == 0:
 
258
            parts.append(" no packages (!)")
 
259
        else:
 
260
            parts.append(" %d package(s)" % len(source_packages))
 
261
        parts.append(
 
262
            " from %s/%s" % (
 
263
                self.source_archive.distribution.name,
 
264
                self.source_archive.name))
 
265
        parts.append(
 
266
            " to %s/%s" % (
 
267
                self.target_archive.distribution.name,
 
268
                self.target_archive.name))
 
269
        parts.append(
 
270
            ", %s pocket," % self.target_pocket.name)
 
271
        if self.target_distroseries is not None:
 
272
            parts.append(" in %s" % self.target_distroseries)
 
273
        if self.include_binaries:
 
274
            parts.append(", including binaries")
 
275
        return "<%s>" % "".join(parts)