]> git.ipfire.org Git - people/jschlag/pbs.git/blame - src/buildservice/jobs.py
jobs: Mark them as deleted instead of removing them instantly
[people/jschlag/pbs.git] / src / buildservice / jobs.py
CommitLineData
2a1e9ce2
MT
1#!/usr/bin/python
2
3import datetime
4import hashlib
5import logging
6import os
7import shutil
8import uuid
9
10import pakfire
11import pakfire.config
12
13log = logging.getLogger("builds")
14log.propagate = 1
15
16from . import arches
17from . import base
18from . import logs
19from . import users
20
21from .constants import *
22from .decorators import *
23
24class Jobs(base.Object):
25 def _get_job(self, query, *args):
26 res = self.db.get(query, *args)
27
28 if res:
29 return Job(self.backend, res.id, data=res)
30
31 def _get_jobs(self, query, *args):
32 res = self.db.query(query, *args)
33
34 for row in res:
35 yield Job(self.backend, row.id, data=row)
36
3f516e41 37 def create(self, build, arch, type="build", superseeds=None):
2a1e9ce2
MT
38 job = self._get_job("INSERT INTO jobs(uuid, type, build_id, arch, time_created) \
39 VALUES(%s, %s, %s, %s, NOW()) RETURNING *", "%s" % uuid.uuid4(), type, build.id, arch)
40 job.log("created")
41
42 # Set cache for Build object.
43 job.build = build
44
3f516e41
MT
45 # Mark if the new job superseeds some other job
46 if superseeds:
47 superseeds.superseeded_by = job
48
2a1e9ce2
MT
49 # Jobs are by default in state "new" and wait for being checked
50 # for dependencies. Packages that do have no build dependencies
51 # can directly be forwarded to "pending" state.
52 if not job.pkg.requires:
53 job.state = "pending"
54
55 return job
56
57 def get_by_id(self, id, data=None):
58 return Job(self.backend, id, data)
59
60 def get_by_uuid(self, uuid):
61 job = self.db.get("SELECT id FROM jobs WHERE uuid = %s", uuid)
62
63 if job:
64 return self.get_by_id(job.id)
65
66 def get_by_build(self, build_id, build=None, type=None):
67 """
68 Get all jobs in the specifies build.
69 """
70 query = "SELECT * FROM jobs WHERE build_id = %s"
71 args = [build_id,]
72
73 if type:
74 query += " AND type = %s"
75 args.append(type)
76
77 # Get IDs of all builds in this group.
78 jobs = []
79 for job in self.db.query(query, *args):
80 job = Job(self.backend, job.id, job)
81
82 # If the Build object was set, we set it so it won't be retrieved
83 # from the database again.
84 if build:
85 job._build = build
86
87 jobs.append(job)
88
89 # Return sorted list of jobs.
90 return sorted(jobs)
91
92 def get_active(self, host_id=None, builder=None, states=None):
93 if builder:
94 host_id = builder.id
95
96 if states is None:
97 states = ["dispatching", "running", "uploading"]
98
99 query = "SELECT * FROM jobs WHERE state IN (%s)" % ", ".join(["%s"] * len(states))
100 args = states
101
102 if host_id:
103 query += " AND builder_id = %s" % host_id
104
105 query += " ORDER BY \
106 CASE \
107 WHEN jobs.state = 'running' THEN 0 \
108 WHEN jobs.state = 'uploading' THEN 1 \
109 WHEN jobs.state = 'dispatching' THEN 2 \
110 WHEN jobs.state = 'pending' THEN 3 \
111 WHEN jobs.state = 'new' THEN 4 \
112 END, time_started ASC"
113
114 return [Job(self.backend, j.id, j) for j in self.db.query(query, *args)]
115
116 def get_latest(self, arch=None, builder=None, limit=None, age=None, date=None):
117 query = "SELECT * FROM jobs"
118 args = []
119
120 where = ["(state = 'finished' OR state = 'failed' OR state = 'aborted')"]
121
122 if arch:
123 where.append("arch = %s")
124 args.append(arch)
125
126 if builder:
127 where.append("builder_id = %s")
128 args.append(builder.id)
129
130 if date:
131 try:
132 year, month, day = date.split("-", 2)
133 date = datetime.date(int(year), int(month), int(day))
134 except ValueError:
135 pass
136 else:
137 where.append("(time_created::date = %s OR \
138 time_started::date = %s OR time_finished::date = %s)")
139 args += (date, date, date)
140
141 if age:
142 where.append("time_finished >= NOW() - '%s'::interval" % age)
143
144 if where:
145 query += " WHERE %s" % " AND ".join(where)
146
147 query += " ORDER BY time_finished DESC"
148
149 if limit:
150 query += " LIMIT %s"
151 args.append(limit)
152
153 return [Job(self.backend, j.id, j) for j in self.db.query(query, *args)]
154
155 def get_average_build_time(self):
156 """
157 Returns the average build time of all finished builds from the
158 last 3 months.
159 """
160 result = self.db.get("SELECT AVG(time_finished - time_started) as average \
161 FROM jobs WHERE type = 'build' AND state = 'finished' AND \
162 time_finished >= NOW() - '3 months'::interval")
163
164 if result:
165 return result.average
166
167 def count(self, *states):
168 query = "SELECT COUNT(*) AS count FROM jobs"
169 args = []
170
171 if states:
172 query += " WHERE state IN %s"
173 args.append(states)
174
175 jobs = self.db.get(query, *args)
176 if jobs:
177 return jobs.count
178
179 def restart_failed(self, max_tries=9):
180 jobs = self._get_jobs("SELECT jobs.* FROM jobs \
181 JOIN builds ON builds.id = jobs.build_id \
182 WHERE \
183 jobs.type = 'build' AND \
184 jobs.state = 'failed' AND \
185 jobs.tries <= %s AND \
186 NOT builds.state = 'broken' AND \
187 jobs.time_finished < NOW() - '72 hours'::interval \
188 ORDER BY \
189 CASE \
190 WHEN jobs.type = 'build' THEN 0 \
191 WHEN jobs.type = 'test' THEN 1 \
192 END, \
193 builds.priority DESC, jobs.time_created ASC",
194 max_tries)
195
196 # Restart the job
197 for job in jobs:
198 job.set_state("new", log=False)
199
200
201class Job(base.DataObject):
202 table = "jobs"
203
204 def __str__(self):
205 return "<%s id=%s %s>" % (self.__class__.__name__, self.id, self.name)
206
207 def __eq__(self, other):
208 if isinstance(other, self.__class__):
209 return self.id == other.id
210
211 def __lt__(self, other):
212 if isinstance(other, self.__class__):
4f90cf84 213 if not self.test and other.test:
2a1e9ce2
MT
214 return True
215
216 if self.build == other.build:
217 return arches.priority(self.arch) < arches.priority(other.arch)
218
219 return self.time_created < other.time_created
220
221 def __iter__(self):
222 packages = self.backend.packages._get_packages("SELECT packages.* FROM jobs_packages \
223 LEFT JOIN packages ON jobs_packages.pkg_id = packages.id \
224 WHERE jobs_packages.job_id = %s ORDER BY packages.name", self.id)
225
226 return iter(packages)
227
228 def __nonzero__(self):
229 return True
230
231 def __len__(self):
232 res = self.db.get("SELECT COUNT(*) AS len FROM jobs_packages \
233 WHERE job_id = %s", self.id)
234
235 return res.len
236
237 @property
238 def distro(self):
239 return self.build.distro
240
3f516e41
MT
241 def get_superseeded_by(self):
242 if self.data.superseeded_by:
243 return self.backend.jobs.get_by_id(self.data.superseeded_by)
244
245 def set_superseeded_by(self, superseeded_by):
246 assert isinstance(superseeded_by, self.__class__)
247
248 self._set_attribute("superseeded_by", superseeded_by.id)
249 self.superseeded_by = superseeded_by
250
251 superseeded_by = lazy_property(get_superseeded_by, set_superseeded_by)
252
2a1e9ce2 253 def delete(self):
40a82ce5
MT
254 self._set_attribute("delete", True)
255
256 def remove(self):
257 """
258 Removes a job from the database
259 """
260 self.__remove_buildroots()
261 self.__remove_history()
262 self.__remove_packages()
263 self.__remove_logfiles()
2a1e9ce2
MT
264
265 # Delete the job itself.
266 self.db.execute("DELETE FROM jobs WHERE id = %s", self.id)
267
40a82ce5 268 def __remove_buildroots(self):
2a1e9ce2
MT
269 """
270 Removes all buildroots.
271 """
272 self.db.execute("DELETE FROM jobs_buildroots WHERE job_id = %s", self.id)
273
40a82ce5 274 def __remove_history(self):
2a1e9ce2
MT
275 """
276 Removes all references in the history to this build job.
277 """
278 self.db.execute("DELETE FROM jobs_history WHERE job_id = %s", self.id)
279
40a82ce5 280 def __remove_packages(self):
2a1e9ce2
MT
281 """
282 Deletes all uploaded files from the job.
283 """
284 for pkg in self.packages:
285 pkg.delete()
286
287 self.db.execute("DELETE FROM jobs_packages WHERE job_id = %s", self.id)
288
40a82ce5 289 def __remove_logfiles(self):
2a1e9ce2
MT
290 for logfile in self.logfiles:
291 self.db.execute("INSERT INTO queue_delete(path) VALUES(%s)", logfile.path)
292
293 def reset(self, user=None):
40a82ce5
MT
294 self.__remove_buildroots()
295 self.__remove_packages()
296 self.__remove_history()
297 self.__remove_logfiles()
2a1e9ce2
MT
298
299 self.state = "new"
300 self.log("reset", user=user)
301
302 ## Logging stuff
303
304 def log(self, action, user=None, state=None, builder=None, test_job=None):
305 user_id = None
306 if user:
307 user_id = user.id
308
309 builder_id = None
310 if builder:
311 builder_id = builder.id
312
313 test_job_id = None
314 if test_job:
315 test_job_id = test_job.id
316
317 self.db.execute("INSERT INTO jobs_history(job_id, action, state, user_id, \
318 time, builder_id, test_job_id) VALUES(%s, %s, %s, %s, NOW(), %s, %s)",
319 self.id, action, state, user_id, builder_id, test_job_id)
320
321 def get_log(self, limit=None, offset=None, user=None):
322 query = "SELECT * FROM jobs_history"
323
324 conditions = ["job_id = %s",]
325 args = [self.id,]
326
327 if user:
328 conditions.append("user_id = %s")
329 args.append(user.id)
330
331 if conditions:
332 query += " WHERE %s" % " AND ".join(conditions)
333
334 query += " ORDER BY time DESC"
335
336 if limit:
337 if offset:
338 query += " LIMIT %s,%s"
339 args += [offset, limit,]
340 else:
341 query += " LIMIT %s"
342 args += [limit,]
343
344 entries = []
345 for entry in self.db.query(query, *args):
346 entry = logs.JobLogEntry(self.backend, entry)
347 entries.append(entry)
348
349 return entries
350
351 @property
352 def uuid(self):
353 return self.data.uuid
354
355 @property
4f90cf84
MT
356 def test(self):
357 return self.data.test
2a1e9ce2
MT
358
359 @property
360 def build_id(self):
361 return self.data.build_id
362
363 @lazy_property
364 def build(self):
365 return self.backend.builds.get_by_id(self.build_id)
366
367 @property
368 def related_jobs(self):
369 ret = []
370
371 for job in self.build.jobs:
372 if job == self:
373 continue
374
375 ret.append(job)
376
377 return ret
378
379 @property
380 def pkg(self):
381 return self.build.pkg
382
383 @property
384 def name(self):
385 return "%s-%s.%s" % (self.pkg.name, self.pkg.friendly_version, self.arch)
386
387 @property
388 def size(self):
389 return sum((p.size for p in self.packages))
390
391 @lazy_property
392 def rank(self):
393 """
394 Returns the rank in the build queue
395 """
396 if not self.state == "pending":
397 return
398
399 res = self.db.get("SELECT rank FROM jobs_queue WHERE job_id = %s", self.id)
400
401 if res:
402 return res.rank
403
404 def is_running(self):
405 """
406 Returns True if job is in a running state.
407 """
408 return self.state in ("pending", "dispatching", "running", "uploading")
409
410 def get_state(self):
411 return self.data.state
412
413 def set_state(self, state, user=None, log=True):
414 # Nothing to do if the state remains.
415 if not self.state == state:
416 self.db.execute("UPDATE jobs SET state = %s WHERE id = %s", state, self.id)
417
418 # Log the event.
419 if log and not state == "new":
420 self.log("state_change", state=state, user=user)
421
422 # Update cache.
423 if self._data:
424 self._data["state"] = state
425
426 # Always clear the message when the status is changed.
427 self.update_message(None)
428
429 # Update some more informations.
430 if state == "dispatching":
431 # Set start time.
432 self.db.execute("UPDATE jobs SET time_started = NOW(), time_finished = NULL \
433 WHERE id = %s", self.id)
434
435 elif state == "pending":
436 self.db.execute("UPDATE jobs SET tries = tries + 1, time_started = NULL, \
437 time_finished = NULL WHERE id = %s", self.id)
438
439 elif state in ("aborted", "dependency_error", "finished", "failed"):
440 # Set finish time and reset builder..
441 self.db.execute("UPDATE jobs SET time_finished = NOW() WHERE id = %s", self.id)
442
443 # Send messages to the user.
444 if state == "finished":
445 self.send_finished_message()
446
447 elif state == "failed":
448 # Remove all package files if a job is set to failed state.
449 self.__delete_packages()
450
451 self.send_failed_message()
452
453 # Automatically update the state of the build (not on test builds).
4f90cf84 454 if not self.test:
2a1e9ce2
MT
455 self.build.auto_update_state()
456
457 state = property(get_state, set_state)
458
459 @property
460 def message(self):
461 return self.data.message
462
463 def update_message(self, msg):
464 self.db.execute("UPDATE jobs SET message = %s WHERE id = %s",
465 msg, self.id)
466
467 if self._data:
468 self._data["message"] = msg
469
470 def get_builder(self):
471 if self.data.builder_id:
472 return self.backend.builders.get_by_id(self.data.builder_id)
473
474 def set_builder(self, builder, user=None):
475 self.db.execute("UPDATE jobs SET builder_id = %s WHERE id = %s",
476 builder.id, self.id)
477
478 # Update cache.
479 if self._data:
480 self._data["builder_id"] = builder.id
481
482 self._builder = builder
483
484 # Log the event.
485 if user:
486 self.log("builder_assigned", builder=builder, user=user)
487
488 builder = lazy_property(get_builder, set_builder)
489
490 @property
491 def arch(self):
492 return self.data.arch
493
494 @property
495 def duration(self):
496 if not self.time_started:
497 return 0
498
499 if self.time_finished:
500 delta = self.time_finished - self.time_started
501 else:
502 delta = datetime.datetime.utcnow() - self.time_started
503
504 return delta.total_seconds()
505
506 @property
507 def time_created(self):
508 return self.data.time_created
509
510 @property
511 def time_started(self):
512 return self.data.time_started
513
514 @property
515 def time_finished(self):
516 return self.data.time_finished
517
518 @property
519 def expected_runtime(self):
520 """
521 Returns the estimated time and stddev, this job takes to finish.
522 """
523 # Get the average build time.
524 build_times = self.backend.builds.get_build_times_by_arch(self.arch,
525 name=self.pkg.name)
526
527 # If there is no statistical data, we cannot estimate anything.
528 if not build_times:
529 return None, None
530
531 return build_times.average, build_times.stddev
532
533 @property
534 def eta(self):
535 expected_runtime, stddev = self.expected_runtime
536
537 if expected_runtime:
538 return expected_runtime - int(self.duration), stddev
539
540 @property
541 def tries(self):
542 return self.data.tries
543
544 def get_pkg_by_uuid(self, uuid):
545 pkg = self.backend.packages._get_package("SELECT packages.id FROM packages \
546 JOIN jobs_packages ON jobs_packages.pkg_id = packages.id \
547 WHERE jobs_packages.job_id = %s AND packages.uuid = %s",
548 self.id, uuid)
549
550 if pkg:
551 pkg.job = self
552 return pkg
553
554 @lazy_property
555 def logfiles(self):
556 logfiles = []
557
558 for log in self.db.query("SELECT id FROM logfiles WHERE job_id = %s", self.id):
559 log = logs.LogFile(self.backend, log.id)
560 log._job = self
561
562 logfiles.append(log)
563
564 return logfiles
565
566 def add_file(self, filename):
567 """
568 Add the specified file to this job.
569
570 The file is copied to the right directory by this function.
571 """
572 assert os.path.exists(filename)
573
574 if filename.endswith(".log"):
575 self._add_file_log(filename)
576
577 elif filename.endswith(".%s" % PACKAGE_EXTENSION):
578 # It is not allowed to upload packages on test builds.
4f90cf84 579 if self.test:
2a1e9ce2
MT
580 return
581
582 self._add_file_package(filename)
583
584 def _add_file_log(self, filename):
585 """
586 Attach a log file to this job.
587 """
588 target_dirname = os.path.join(self.build.path, "logs")
589
4f90cf84 590 if self.test:
2a1e9ce2
MT
591 i = 1
592 while True:
593 target_filename = os.path.join(target_dirname,
594 "test.%s.%s.%s.log" % (self.arch, i, self.tries))
595
596 if os.path.exists(target_filename):
597 i += 1
598 else:
599 break
600 else:
601 target_filename = os.path.join(target_dirname,
602 "build.%s.%s.log" % (self.arch, self.tries))
603
604 # Make sure the target directory exists.
605 if not os.path.exists(target_dirname):
606 os.makedirs(target_dirname)
607
608 # Calculate a SHA512 hash from that file.
609 f = open(filename, "rb")
610 h = hashlib.sha512()
611 while True:
612 buf = f.read(BUFFER_SIZE)
613 if not buf:
614 break
615
616 h.update(buf)
617 f.close()
618
619 # Copy the file to the final location.
620 shutil.copy2(filename, target_filename)
621
622 # Create an entry in the database.
623 self.db.execute("INSERT INTO logfiles(job_id, path, filesize, hash_sha512) \
624 VALUES(%s, %s, %s, %s)", self.id, os.path.relpath(target_filename, PACKAGES_DIR),
625 os.path.getsize(target_filename), h.hexdigest())
626
627 def _add_file_package(self, filename):
628 # Open package (creates entry in the database)
629 pkg = self.backend.packages.create(filename)
630
631 # Move package to the build directory.
632 pkg.move(os.path.join(self.build.path, self.arch))
633
634 # Attach the package to this job.
635 self.db.execute("INSERT INTO jobs_packages(job_id, pkg_id) VALUES(%s, %s)",
636 self.id, pkg.id)
637
638 def get_aborted_state(self):
639 return self.data.aborted_state
640
641 def set_aborted_state(self, state):
642 self._set_attribute("aborted_state", state)
643
644 aborted_state = property(get_aborted_state, set_aborted_state)
645
646 @property
647 def message_recipients(self):
648 l = []
649
650 # Add all people watching the build.
651 l += self.build.message_recipients
652
653 # Add the package maintainer on release builds.
654 if self.build.type == "release":
655 maint = self.pkg.maintainer
656
657 if isinstance(maint, users.User):
658 l.append("%s <%s>" % (maint.realname, maint.email))
659 elif maint:
660 l.append(maint)
661
662 # XXX add committer and commit author.
663
664 # Add the owner of the scratch build on scratch builds.
665 elif self.build.type == "scratch" and self.build.user:
666 l.append("%s <%s>" % \
667 (self.build.user.realname, self.build.user.email))
668
669 return set(l)
670
671 def save_buildroot(self, pkgs):
672 rows = []
673
674 for pkg_name, pkg_uuid in pkgs:
675 rows.append((self.id, self.tries, pkg_uuid, pkg_name))
676
677 # Cleanup old stuff first (for rebuilding packages).
678 self.db.execute("DELETE FROM jobs_buildroots WHERE job_id = %s AND tries = %s",
679 self.id, self.tries)
680
681 self.db.executemany("INSERT INTO \
682 jobs_buildroots(job_id, tries, pkg_uuid, pkg_name) \
683 VALUES(%s, %s, %s, %s)", rows)
684
685 def has_buildroot(self, tries=None):
686 if tries is None:
687 tries = self.tries
688
689 res = self.db.get("SELECT COUNT(*) AS num FROM jobs_buildroots \
690 WHERE jobs_buildroots.job_id = %s AND jobs_buildroots.tries = %s",
691 self.id, tries)
692
693 if res:
694 return res.num
695
696 return 0
697
698 def get_buildroot(self, tries=None):
699 if tries is None:
700 tries = self.tries
701
702 rows = self.db.query("SELECT * FROM jobs_buildroots \
703 WHERE jobs_buildroots.job_id = %s AND jobs_buildroots.tries = %s \
704 ORDER BY pkg_name", self.id, tries)
705
706 pkgs = []
707 for row in rows:
708 # Search for this package in the packages table.
709 pkg = self.backend.packages.get_by_uuid(row.pkg_uuid)
710 pkgs.append((row.pkg_name, row.pkg_uuid, pkg))
711
712 return pkgs
713
714 def send_finished_message(self):
715 # Send no finished mails for test jobs.
4f90cf84 716 if self.test:
2a1e9ce2
MT
717 return
718
719 logging.debug("Sending finished message for job %s to %s" % \
720 (self.name, ", ".join(self.message_recipients)))
721
722 info = {
723 "build_name" : self.name,
724 "build_host" : self.builder.name,
725 "build_uuid" : self.uuid,
726 }
727
728 self.backend.messages.send_to_all(self.message_recipients,
729 MSG_BUILD_FINISHED_SUBJECT, MSG_BUILD_FINISHED, info)
730
731 def send_failed_message(self):
732 logging.debug("Sending failed message for job %s to %s" % \
733 (self.name, ", ".join(self.message_recipients)))
734
735 build_host = "--"
736 if self.builder:
737 build_host = self.builder.name
738
739 info = {
740 "build_name" : self.name,
741 "build_host" : build_host,
742 "build_uuid" : self.uuid,
743 }
744
745 self.backend.messages.send_to_all(self.message_recipients,
746 MSG_BUILD_FAILED_SUBJECT, MSG_BUILD_FAILED, info)
747
748 def set_start_time(self, start_not_before):
749 self._set_attribute("start_not_before", start_not_before)
750
751 def schedule(self, type, start_time=None, user=None):
752 assert type in ("rebuild", "test")
753
754 if type == "rebuild":
755 if self.state == "finished":
756 return
757
758 self.set_state("new", user=user, log=False)
759 self.set_start_time(start_time)
760
761 # Log the event.
762 self.log("schedule_rebuild", user=user)
763
764 elif type == "test":
765 if not self.state == "finished":
766 return
767
768 # Create a new job with same build and arch.
769 job = self.create(self.backend, self.build, self.arch, type="test")
770 job.set_start_time(start_time)
771
772 # Log the event.
773 self.log("schedule_test_job", test_job=job, user=user)
774
775 return job
776
777 def schedule_test(self, start_not_before=None, user=None):
778 # XXX to be removed
779 return self.schedule("test", start_time=start_not_before, user=user)
780
781 def schedule_rebuild(self, start_not_before=None, user=None):
782 # XXX to be removed
783 return self.schedule("rebuild", start_time=start_not_before, user=user)
784
785 def get_build_repos(self):
786 """
787 Returns a list of all repositories that should be used when
788 building this job.
789 """
790 repo_ids = self.db.query("SELECT repo_id FROM jobs_repos WHERE job_id = %s",
791 self.id)
792
793 if not repo_ids:
794 return self.distro.get_build_repos()
795
796 repos = []
797 for repo in self.distro.repositories:
798 if repo.id in [r.id for r in repo_ids]:
799 repos.append(repo)
800
801 return repos or self.distro.get_build_repos()
802
803 def get_repo_config(self):
804 """
805 Get repository configuration file that is sent to the builder.
806 """
807 confs = []
808
809 for repo in self.get_build_repos():
810 confs.append(repo.get_conf())
811
812 return "\n\n".join(confs)
813
814 def get_config(self):
815 """
816 Get configuration file that is sent to the builder.
817 """
818 confs = []
819
820 # Add the distribution configuration.
821 confs.append(self.distro.get_config())
822
823 # Then add all repositories for this build.
824 confs.append(self.get_repo_config())
825
826 return "\n\n".join(confs)
827
828 def resolvdep(self):
829 config = pakfire.config.Config(files=["general.conf"])
830 config.parse(self.get_config())
831
832 # The filename of the source file.
833 filename = os.path.join(PACKAGES_DIR, self.build.pkg.path)
834 assert os.path.exists(filename), filename
835
836 # Create a new pakfire instance with the configuration for
837 # this build.
838 p = pakfire.PakfireServer(config=config, arch=self.arch)
839
840 # Try to solve the build dependencies.
841 try:
842 solver = p.resolvdep(filename)
843
844 # Catch dependency errors and log the problem string.
845 except DependencyError, e:
846 self.state = "dependency_error"
847 self.update_message(e)
848
849 else:
850 # If the build dependencies can be resolved, we set the build in
851 # pending state.
852 if solver.status is True:
853 if self.state in ("failed",):
854 return
855
856 self.state = "pending"