~launchpad-pqm/launchpad/devel

« back to all changes in this revision

Viewing changes to lib/importd/util.py

  • Committer: Robert Collins
  • Date: 2005-10-31 18:29:12 UTC
  • mfrom: (1102.1.126)
  • mto: (1102.1.138) (63.1.155)
  • mto: This revision was merged to the branch mainline in revision 2836.
  • Revision ID: robertc@robertcollins.net-20051031182912-5b96cbfc568d7a46
Merge ddaa and my branch.

Show diffs side-by-side

added added

removed removed

Lines of Context:
130
130
            'periodicBuildTime': job.frequency})
131
131
    return builders
132
132
 
133
 
def productJobsDict(jobs):
134
 
    """Mapping of products to jobs.
135
 
 
136
 
    :type jobs: iterable of importd.Job.Job
137
 
    :return: mapping from product ids to jobs.
138
 
    :rtype: dict of int to buildbot.importd.Job.Job
139
 
    """
140
 
    by_product = {}
141
 
    for job in jobs:
142
 
        product_jobs = by_product.get(job.product_id, [])
143
 
        product_jobs.append(job)
144
 
        by_product[job.product_id] = product_jobs
145
 
    return by_product
146
 
 
147
 
def anonymousInterlock(jobs):
148
 
    """Create anonymous interlock between for sources and packages.
149
 
 
150
 
    :type jobs: iterable of importd.Job.Job
151
 
    :return: anonymous interlock ([sources], [packages]), or ``None`` if there
152
 
        are no sources or no packages.
153
 
    :rtype: 2-tuple of lists of importd.Job.Job or ``None``
154
 
    """
155
 
    sources, packages = [], []
156
 
    for job in jobs:
157
 
        if job.RCS == 'package':
158
 
            packages.append(job.name)
159
 
        elif job.RCS in ('cvs', 'svn'):
160
 
            sources.append(job.name)
161
 
        else:
162
 
            raise RuntimeError('RCS of job %r is %r'
163
 
                               % (job.name, job.RCS))
164
 
    if len(sources) == 0 or len(packages) == 0:
165
 
        return None
166
 
    else:
167
 
        return sources, packages
168
 
 
169
 
def nameInterlock(product_id, interlock):
170
 
    """Put the product name into an anonymous interlock.
171
 
 
172
 
    :param product_id: id of the product associated to the interlock
173
 
    :param interlock: ([feeders], [watchers]) pair
174
 
    :precondition: all jobs in the interlock have product_id.
175
 
    :return: named interlock, ("name", [feeders], [watchers])
176
 
    """
177
 
    feeders, watchers = interlock
178
 
    name = Product.get(product_id).name
179
 
    return name, feeders, watchers
180
 
 
181
 
def jobsInterlocks(jobs):
182
 
    """Create interlocks for the given jobs.
183
 
 
184
 
    :param jobs: jobs to create interlocks for.
185
 
    :type jobs: iterable of importd.Job.Job
186
 
    :return: interlocks for BuildmasterConfig.
187
 
    :rtype: list of (name, [feeders], [watchers]) tuples
188
 
    """
189
 
    interlocks = []
190
 
    by_product = productJobsDict(jobs)
191
 
    for product_id, product_jobs in by_product.items():
192
 
        anon = anonymousInterlock(product_jobs)
193
 
        if anon is not None:
194
 
            inter = nameInterlock(product_id, anon)
195
 
            interlocks.append(inter)
196
 
    return interlocks
197
 
 
198
 
def processDir(builders, log, basedir, tree, slavename, slave_home,
199
 
               archive_mirror_dir):
200
 
    log.msg("checking dir " + basedir)
201
 
    builder = JobBuilder()
202
 
    for name in tree.iter_inventory(source=True, files=True):
203
 
        jobs = builder.jobFromFile(os.path.join(basedir,name))
204
 
        for (job, jobname) in jobs:
205
 
            job.jobname = jobname
206
 
            job.name = jobname
207
 
            job.slave_home=slave_home
208
 
            job.archive_mirror_dir = archive_mirror_dir
209
 
            f1 = ImportDBuildFactory(job, jobname)
210
 
            builders.append((jobname, slavename, "buildbot-jobs", f1,
211
 
                             {'periodicBuildTime':job.frequency}))
212
133
 
213
134
 
214
135
from twisted.python.failure import Failure
215
136
 
216
 
class ImportDBuild(ConfigurableBuild):
217
 
    """I am a build that informs the database of success and failure."""
 
137
class NotifyingBuild(ConfigurableBuild):
 
138
    """Build that notifies of starts and finishes and can refresh itself.
 
139
    """
 
140
 
 
141
    def getObserver(self):
 
142
        raise NotImplementedError
218
143
 
219
144
    def startBuild(self, remote, progress):
220
145
        self.__finished = False
221
146
        try:
222
 
            ImportDBImplementor(self).startBuild()
 
147
            self.getObserver().startBuild()
223
148
        except:
224
149
            f = Failure()
225
150
            tryToAbortTransaction()
228
153
 
229
154
    def buildFinished(self, event, successful=1):
230
155
        if not self.__finished:
231
 
            # catch recursive calls caused by a failure in ImportDBImplementor
 
156
            # catch recursive calls caused by a failure in observer
232
157
            self.__finished = True
233
158
            try:
234
 
                ImportDBImplementor(self).buildFinished(successful)
 
159
                self.getObserver().buildFinished(successful)
235
160
            except:
236
161
                f = Failure()
237
162
                tryToAbortTransaction()
239
164
                self.buildException(f, "buildFinished")
240
165
        ConfigurableBuild.buildFinished(self, event, successful)
241
166
 
 
167
    def refreshBuilder(self, rerun, periodic):
 
168
        self.builder.stopPeriodicBuildTimer()
 
169
        # change the builder and run it again after the buildFinished 
 
170
        # process finishes.
 
171
        # XXX: This should not be needed. It is needed because we are
 
172
        # in a deep call stack which will call into
 
173
        # self.build.builder.expectations which is currently coupled
 
174
        # to the value of self.build.builder.steps.
 
175
        # If this is fixed, we can simply call self.refreshBuilder().
 
176
        reactor.callLater(1, self.refreshBuilderDelayed, rerun, periodic)
 
177
 
 
178
    def refreshBuilderDelayed(self, rerun, periodic):
 
179
        """refresh the builder and then force a build"""
 
180
        # This might be better as a helper function in the module, but that
 
181
        # feels more unclean than duplicating these lines as they come from
 
182
        # several not-well-connected places
 
183
        self.builder.buildFactory.steps = []
 
184
        self.builder.buildFactory.addSteps()
 
185
        self.builder.waiting = self.builder.newBuild()
 
186
        self.builder.expectations = None
 
187
        p = self.builder.waiting.setupProgress()
 
188
        if p:
 
189
            self.builder.expectations = Expectations(p)
 
190
        self.builder.periodicBuildTime = periodic
 
191
        self.builder.startPeriodicBuildTimer()
 
192
        if rerun:
 
193
            self.builder.forceBuild("botmaster", "import completed",
 
194
                                    periodic=False)
 
195
 
 
196
 
 
197
class ImportDBuild(NotifyingBuild):
 
198
    """Build that updates the database with RCS import status."""
 
199
 
 
200
    def getObserver(self):
 
201
        return ImportDBImplementor(self)
 
202
 
242
203
 
243
204
class ImportDBImplementor(object):
244
205
    """Implementation of the ImportDBuild behaviour, separates namespaces.
294
255
            series.importstatus = ImportStatus.AUTOTESTED
295
256
        else:
296
257
            series.importstatus = ImportStatus.TESTFAILED
297
 
        self.refreshBuilder(rerun = False)
 
258
        self.refreshBuilder(rerun=False)
298
259
 
299
260
    def processingComplete(self, successful):
300
 
        """Impot or sync run is complete, update database and buildbot.
 
261
        """Import or sync run is complete, update database and buildbot.
301
262
 
302
263
        If the job was an import, make it a sync and rerun it immediately.
303
264
        """
310
271
            self.refreshBuilder(rerun = True)
311
272
 
312
273
    def refreshBuilder(self, rerun):
313
 
        self.build.builder.stopPeriodicBuildTimer()
314
 
        # change the builder and run it again after the buildFinished 
315
 
        # process finishes.
316
 
        # XXX: This should not be needed. It is needed because we are
317
 
        # in a deep call stack which will call into
318
 
        # self.build.builder.expectations which is currently coupled
319
 
        # to the value of self.build.builder.steps.
320
 
        # If this is fixed, we can simply call self.refreshBuilder().
321
 
        reactor.callLater(1, self.refreshBuilderDelayed, rerun)
322
 
 
323
 
    def refreshBuilderDelayed(self, rerun):
324
 
        """refresh the builder and then force a build"""
325
 
        job=self.build.importDJob
326
 
        # This might be better as a helper function in the module, but that
327
 
        # feels more unclean than duplicating these lines as they come from
328
 
        # several not-well-connected places
329
 
        self.build.builder.buildFactory.steps = []
330
 
        self.build.builder.buildFactory.addSteps()
331
 
        self.build.builder.waiting = self.build.builder.newBuild()
332
 
        self.build.builder.expectations = None
333
 
        p = self.build.builder.waiting.setupProgress()
334
 
        if p:
335
 
            self.build.builder.expectations = Expectations(p)
336
 
        self.build.builder.periodicBuildTime = self.build.importDJob.frequency
337
 
        self.build.builder.startPeriodicBuildTimer()
338
 
        if rerun:
339
 
            self.build.builder.forceBuild("botmaster", "import completed",
340
 
                                          periodic=False)
 
274
        periodic = self.build.importDJob.frequency
 
275
        self.build.refreshBuilder(rerun=rerun, periodic=periodic)
341
276
 
342
277
 
343
278
class ImportDBuildFactory(ConfigurableBuildFactory):
352
287
        self.addSteps()
353
288
 
354
289
    def addSteps(self):
355
 
        if self.job.RCS == 'package':
356
 
            self.addImportDStep('runJob')
357
 
        elif self.job.TYPE == "import":
 
290
        if self.job.TYPE == "import":
358
291
            self.addImportDStep('nukeTargets')
359
292
            self.addImportDStep('runJob')
360
293
        elif self.job.TYPE == 'sync':
363
296
                self.addImportDStep('mirrorTarget')
364
297
 
365
298
    def addImportDStep(self, method):
366
 
        steps.append((RunJobStep, {
367
 
            'job': self.job,
368
 
            'method': 'mirrorTarget',
369
 
            'workdir': self.jobfile}))
 
299
        raise NotImplementedError
370
300
 
371
301
    def newBuild(self):
372
 
        result=ConfigurableBuildFactory.newBuild(self)
373
 
        result.importDJob=self.job
 
302
        # Save the job inside the build, so the startBuild and buildFinished
 
303
        # handlers can use it
 
304
        result = ConfigurableBuildFactory.newBuild(self)
 
305
        result.importDJob = self.job
374
306
        return result
375
307
 
376
308
 
526
458
                                  "job", args)
527
459
        # might raise UnknownCommand if it isn't implemented
528
460
        d.addErrback(self.stepFailed)
529
 
 
530
 
 
531
 
class RunJobStep(JobBuildStep):
532
 
    """ I 'run' a job as a single operation"""
533
 
 
534
 
    def __init__(self, job, method,workdir=".", **kwargs):
535
 
        JobBuildStep.__init__(self, **kwargs)
536
 
        self.job = job
537
 
        self.workdir=workdir
538
 
        self.log = Logfile()
539
 
        self.method=method
540
 
        self.name="%s" % self.words()
541
 
 
542
 
    def words(self):
543
 
        return ["runJobStep %s" % self.method]
544
 
 
545
 
    def start(self):
546
 
        assert(self.job != None)
547
 
        args = {'job': self.job,
548
 
                'method': self.method,
549
 
                'args': [],
550
 
                'dir': self.workdir
551
 
        }
552
 
        #here, these are all passed to the child's copy of <class>. There is
553
 
        # a case based constructor on the child, which is used to determine
554
 
        # the class to instantiate.
555
 
        #
556
 
        #args = {'command': self.command,
557
 
        #        'dir': self.workdir,
558
 
        #        'env': self.env,
559
 
        #        # env, want_stdout, want_stderr
560
 
        #        'timeout': self.timeout
561
 
        #        }
562
 
        d = self.remote.callRemote("startCommand", self, self.stepId,
563
 
                                  "job", args)
564
 
        # might raise UnknownCommand if it isn't implemented
565
 
        d.addErrback(self.stepFailed)
566
 
 
567
 
 
568
 
class JobBuilder:
569
 
    known_keys=["TYPE","RCS", "repository", "module", "category",
570
 
                "archivename", "branchfrom", "branchto", "frequency"]
571
 
    import re
572
 
    _re_key = re.compile(r'^[ \t]*([a-zA-z]*)[ \t]*=[ \t]*([^ \t].*)[ \t]*$')
573
 
    _re_job = re.compile(r'^.*\.info$')
574
 
 
575
 
    def __init__(self):
576
 
        """setup needed tools"""
577
 
        import logging
578
 
        from importd import LoggingLogAdaptor
579
 
        self.logger=logging.Logger("JobBuilder")
580
 
        self.logger.addHandler(LoggingLogAdaptor(log))
581
 
 
582
 
    def jobFromFile(self, filename):
583
 
        """I parse .job files and return Job objects"""
584
 
        if not self._re_job.match(filename): return []
585
 
        import info2job
586
 
        reload (info2job)
587
 
        log.msg("Building from " + filename)
588
 
        info = info2job.read_info(filename, self.logger)
589
 
        return [ (x, info2job.jobfile_name(info, x))
590
 
                 for x in info2job.iter_jobs(info, self.logger)]