Merge lp:~notnownikki/offspring/exclude-failing-builders into lp:offspring

Proposed by Nicola Heald
Status: Merged
Approved by: Kevin McDermott
Approved revision: 166
Merged at revision: 164
Proposed branch: lp:~notnownikki/offspring/exclude-failing-builders
Merge into: lp:offspring
Diff against target: 380 lines (+232/-7)
8 files modified
config/offspring.cfg (+1/-0)
lib/offspring/master/master.py (+9/-1)
lib/offspring/master/models.py (+1/-0)
lib/offspring/master/tests/helpers.py (+3/-1)
lib/offspring/master/tests/test_master.py (+40/-2)
lib/offspring/web/queuemanager/admin.py (+3/-3)
lib/offspring/web/queuemanager/migrations/0004_auto__add_field_lexbuilder_contiguous_errors.py (+174/-0)
lib/offspring/web/queuemanager/models.py (+1/-0)
To merge this branch: bzr merge lp:~notnownikki/offspring/exclude-failing-builders
Reviewer Review Type Date Requested Status
Kevin McDermott Approve
Review via email: mp+165662@code.launchpad.net

Description of the change

Adds contiguous_errors field to Lexbuilder, increments this when a build fails. Resets to 0 on a successful build. Will not use builders that have failed more that X times in a row (configurable)

To post a comment you must log in.
165. By Nicola Heald

Removed TODO comments

Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

Looks good, nothing major, couple of minors (and the first is a personal preference).

=== modified file 'config/offspring.cfg'
+builder_upper_error_limit: 5

Given that we don't have a lower error limit, "upper" seems redundant here?

=== modified file 'lib/offspring/master/master.py'
+ return self.db_store.find(
+ Lexbuilder, Lexbuilder.is_retired == False,
+ Lexbuilder.contiguous_errors < int(
+ self.config.get("master", "builder_upper_error_limit")))

self.config is a ConfigParser, so you could replace this with

+ Lexbuilder.contiguous_errors < self.config.getint(
+ "master", "builder_upper_error_limit"))

What I was wondering about, is should we expose failing builders through the monitoring API? Or should we send a notification when we knock a builder offline (i.e. when we increment the contiguous_errors above the limit?)

review: Needs Fixing
Revision history for this message
Kevin McDermott (bigkevmcd) :
review: Needs Information
Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

Oh, and as the configuration variable is "limit" then we the query should probably be...

+ Lexbuilder.contiguous_errors <= self.config.getint(
+ "master", "builder_upper_error_limit"))

:-)

166. By Nicola Heald

error limit behaves like a limit now, config option changed to remove _upper from the name.

Revision history for this message
Kevin McDermott (bigkevmcd) wrote :

Looks good now, thanks for the changes :-)

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'config/offspring.cfg'
2--- config/offspring.cfg 2012-02-16 06:44:19 +0000
3+++ config/offspring.cfg 2013-05-24 16:54:37 +0000
4@@ -140,6 +140,7 @@
5 poll_frequency: 20
6 logfile: %(log_dir)s/offspring-master.log
7 launchpad_oauth: %(config_dir)s/launchpad.oauth
8+builder_error_limit: 5
9
10 #===============================================================================
11 # Offspring publisher
12
13=== modified file 'lib/offspring/master/master.py'
14--- lib/offspring/master/master.py 2013-05-23 04:18:49 +0000
15+++ lib/offspring/master/master.py 2013-05-24 16:54:37 +0000
16@@ -11,6 +11,7 @@
17 from storm.locals import create_database, Desc, Store
18
19 from offspring.daemon import LexbuilderDaemon
20+from offspring.enums import ProjectBuildStates
21 from offspring.master.exceptions import SlaveCommunicationError
22 from offspring.master.models import BuildRequest, Lexbuilder
23 from offspring.master import notifications
24@@ -97,7 +98,10 @@
25 self.is_online = False
26
27 def _get_builders(self):
28- return self.db_store.find(Lexbuilder, Lexbuilder.is_retired == False)
29+ return self.db_store.find(
30+ Lexbuilder, Lexbuilder.is_retired == False,
31+ Lexbuilder.contiguous_errors <= self.config.getint(
32+ "master", "builder_error_limit"))
33
34 def scanSlaves(self):
35 """
36@@ -146,6 +150,10 @@
37 logging.error(
38 "PROBLEM DETECTED: A non-fatal problem has"
39 " been detected. See above for details.")
40+ if builder.current_job.result == ProjectBuildStates.FAILED:
41+ builder.contiguous_errors += 1
42+ else:
43+ builder.contiguous_errors = 0
44 builder.current_job = None
45 self.db_store.commit()
46 else:
47
48=== modified file 'lib/offspring/master/models.py'
49--- lib/offspring/master/models.py 2013-05-23 04:18:49 +0000
50+++ lib/offspring/master/models.py 2013-05-24 16:54:37 +0000
51@@ -35,6 +35,7 @@
52 is_active = Bool(default=True)
53 is_okay = Bool(default=True)
54 is_retired = Bool(default=False)
55+ contiguous_errors = Int(default=0)
56 current_job_id = Int()
57 current_job = Reference(current_job_id, "BuildResult.id")
58 notes = Unicode()
59
60=== modified file 'lib/offspring/master/tests/helpers.py'
61--- lib/offspring/master/tests/helpers.py 2013-05-23 04:18:49 +0000
62+++ lib/offspring/master/tests/helpers.py 2013-05-24 16:54:37 +0000
63@@ -91,6 +91,7 @@
64 self.log_dir = self.makeDir()
65 self.config.set("master", "log_dir", self.log_dir)
66 self.config.set("master", "db", self.database_uri)
67+ self.config.set("master", "builder_error_limit", "5")
68
69
70 class BaseOffspringMasterTestCase(LexbuilderMasterTestMixin,
71@@ -178,7 +179,8 @@
72 "current_job_id INTEGER, "
73 "is_okay BOOLEAN NOT NULL, "
74 "is_active BOOLEAN NOT NULL DEFAULT TRUE, "
75- "is_retired BOOLEAN NOT NULL DEFAULT FALSE)")
76+ "is_retired BOOLEAN NOT NULL DEFAULT FALSE,"
77+ "contiguous_errors INTEGER NOT NULL DEFAULT 0)")
78
79 self.connection.execute(
80 "CREATE TABLE buildrequests ("
81
82=== modified file 'lib/offspring/master/tests/test_master.py'
83--- lib/offspring/master/tests/test_master.py 2013-05-23 13:55:56 +0000
84+++ lib/offspring/master/tests/test_master.py 2013-05-24 16:54:37 +0000
85@@ -7,6 +7,7 @@
86 import storm
87 from storm.exceptions import DisconnectionError
88
89+from offspring.enums import ProjectBuildStates
90 from offspring.master.master import LexbuilderMaster
91 from offspring.master.models import (
92 Lexbuilder, BuildResult)
93@@ -40,6 +41,7 @@
94 builder.is_active = kwargs.get("is_active", True)
95 builder.is_okay = kwargs.get("is_okay", True)
96 builder.is_retired = kwargs.get("is_retired", False)
97+ builder.contiguous_errors = kwargs.get("contiguous_errors", 0)
98 builder.machine_type = kwargs.get("arch", self.project.arch)
99 self.db_store.add(builder)
100 return builder
101@@ -118,12 +120,33 @@
102 "Scan cycle completed\n",
103 log_file.getvalue())
104
105- def build_builder_and_slave_with_job_and_result(self):
106+ def test_scan_slaves_error_increments_error_count(self):
107+ """
108+ When a builder has a failed build, the contiguous_errors count
109+ is incremented.
110+ """
111+ master, builder = self.build_builder_and_slave_with_job_and_result(
112+ result=ProjectBuildStates.FAILED)
113+ master.scanSlaves()
114+ self.assertEqual(1, builder.contiguous_errors)
115+
116+ def test_scan_slaves_non_error_resets_error_count(self):
117+ """
118+ When a builder has a non-erroring build, the contiguous_errors
119+ field is reset to 0.
120+ """
121+ master, builder = self.build_builder_and_slave_with_job_and_result(
122+ result=ProjectBuildStates.SUCCESS, contiguous_errors=1)
123+ master.scanSlaves()
124+ self.assertEqual(0, builder.contiguous_errors)
125+
126+ def build_builder_and_slave_with_job_and_result(
127+ self, result=None, contiguous_errors=0):
128 """
129 This builds and returns a LexbuilderMaster with a single slave builder,
130 and a job result.
131 """
132- builder = self.create_builder()
133+ builder = self.create_builder(contiguous_errors=contiguous_errors)
134 builder_mock = self.mocker.patch(builder)
135 builder_mock.scan()
136 self.mocker.result((Slave.STATE_IDLE, Slave.STATE_IDLE))
137@@ -131,6 +154,7 @@
138 build_request = self.create_build_request()
139 build_result = BuildResult(build_request, builder)
140 build_result.name = u"20111115-testing"
141+ build_result.result = result
142 self.db_store.add(build_result)
143
144 builder_mock.getBuildResult()
145@@ -232,6 +256,20 @@
146 self.assertEqual(2, builders.count())
147 self.assertEqual(["BuilderB", "BuilderC"], list(builders.values(Lexbuilder.name)))
148
149+ def test_get_builders_does_not_return_errors_over_threshold(self):
150+ """
151+ Test that builders with contiguous_errors over the values defined in settings
152+ are not returned.
153+ """
154+ master = self.get_master()
155+ builder1 = self.create_builder(name="BuilderA", contiguous_errors=5)
156+ builder2 = self.create_builder(name="BuilderB", contiguous_errors=6)
157+ builder3 = self.create_builder(name="BuilderC")
158+ builders = master._get_builders()
159+ self.assertEqual(2, builders.count())
160+ self.assertEqual(["BuilderA", "BuilderC"], list(builders.values(Lexbuilder.name)))
161+
162+
163 def test_dispatch_builds(self):
164 """
165 dispatchBuilds sends BuildRequests to a slave.
166
167=== modified file 'lib/offspring/web/queuemanager/admin.py'
168--- lib/offspring/web/queuemanager/admin.py 2013-05-23 04:18:49 +0000
169+++ lib/offspring/web/queuemanager/admin.py 2013-05-24 16:54:37 +0000
170@@ -20,8 +20,8 @@
171
172 class LexbuilderAdmin(admin.ModelAdmin):
173 list_display = ('name', 'machine_type', 'is_active', 'is_okay',
174- 'is_retired', 'current_state', 'current_build',
175- 'updated_at')
176+ 'is_retired', 'current_state', 'contiguous_errors',
177+ 'current_build', 'updated_at')
178 list_filter = ['current_state', 'machine_type', 'is_active',
179 'is_okay', 'is_retired']
180 actions=['make_enabled', 'make_disabled']
181@@ -36,7 +36,7 @@
182 make_disabled.short_description = "Mark selected builders as disabled"
183
184 def make_enabled(self, request, queryset):
185- rows_updated = queryset.update(is_active=True)
186+ rows_updated = queryset.update(is_active=True, contiguous_errors=0)
187 if rows_updated == 1:
188 message_bit = "1 lexbuilder was"
189 else:
190
191=== added file 'lib/offspring/web/queuemanager/migrations/0004_auto__add_field_lexbuilder_contiguous_errors.py'
192--- lib/offspring/web/queuemanager/migrations/0004_auto__add_field_lexbuilder_contiguous_errors.py 1970-01-01 00:00:00 +0000
193+++ lib/offspring/web/queuemanager/migrations/0004_auto__add_field_lexbuilder_contiguous_errors.py 2013-05-24 16:54:37 +0000
194@@ -0,0 +1,174 @@
195+# encoding: utf-8
196+import datetime
197+from south.db import db
198+from south.v2 import SchemaMigration
199+from django.db import models
200+
201+class Migration(SchemaMigration):
202+
203+ def forwards(self, orm):
204+
205+ # Adding field 'Lexbuilder.contiguous_errors'
206+ db.add_column('lexbuilders', 'contiguous_errors', self.gf('django.db.models.fields.IntegerField')(default=0), keep_default=False)
207+
208+
209+ def backwards(self, orm):
210+
211+ # Deleting field 'Lexbuilder.contiguous_errors'
212+ db.delete_column('lexbuilders', 'contiguous_errors')
213+
214+
215+ models = {
216+ 'auth.group': {
217+ 'Meta': {'object_name': 'Group'},
218+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
219+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
220+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
221+ },
222+ 'auth.permission': {
223+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
224+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
225+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
226+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
227+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
228+ },
229+ 'auth.user': {
230+ 'Meta': {'object_name': 'User'},
231+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 5, 24, 9, 34, 39, 946400)'}),
232+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
233+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
234+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
235+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
236+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
237+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
238+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
239+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 5, 24, 9, 34, 39, 946337)'}),
240+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
241+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
242+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
243+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
244+ },
245+ 'contenttypes.contenttype': {
246+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
247+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
248+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
249+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
250+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
251+ },
252+ 'django_group_access.accessgroup': {
253+ 'Meta': {'ordering': "('name',)", 'object_name': 'AccessGroup'},
254+ 'auto_share_groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'auto_share_groups_rel_+'", 'blank': 'True', 'to': "orm['django_group_access.AccessGroup']"}),
255+ 'can_be_shared_with': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
256+ 'can_share_with': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['django_group_access.AccessGroup']", 'symmetrical': 'False', 'blank': 'True'}),
257+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
258+ 'members': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False', 'blank': 'True'}),
259+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}),
260+ 'supergroup': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
261+ },
262+ 'queuemanager.buildrequest': {
263+ 'Meta': {'object_name': 'BuildRequest', 'db_table': "'buildrequests'"},
264+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
265+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
266+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.Project']", 'db_column': "'project_name'"}),
267+ 'reason': ('django.db.models.fields.TextField', [], {'max_length': '200', 'blank': 'True'}),
268+ 'requestor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'db_column': "'requestor_id'", 'blank': 'True'}),
269+ 'score': ('django.db.models.fields.IntegerField', [], {'default': '10'})
270+ },
271+ 'queuemanager.buildresult': {
272+ 'Meta': {'object_name': 'BuildResult', 'db_table': "'buildresults'"},
273+ 'builder': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.Lexbuilder']", 'null': 'True', 'blank': 'True'}),
274+ 'dispatched_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
275+ 'finished_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
276+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
277+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
278+ 'notes': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
279+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.Project']", 'db_column': "'project_name'"}),
280+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
281+ 'requested_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
282+ 'requestor': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'db_column': "'requestor_id'", 'blank': 'True'}),
283+ 'result': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
284+ 'started_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
285+ },
286+ 'queuemanager.dailybuildorder': {
287+ 'Meta': {'object_name': 'DailyBuildOrder', 'db_table': "'dailybuildorders'"},
288+ 'hour': ('django.db.models.fields.PositiveIntegerField', [], {}),
289+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
290+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
291+ 'projects': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['queuemanager.Project']", 'symmetrical': 'False'})
292+ },
293+ 'queuemanager.launchpadproject': {
294+ 'Meta': {'object_name': 'LaunchpadProject', 'db_table': "'launchpad_projects'"},
295+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200', 'primary_key': 'True'}),
296+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'})
297+ },
298+ 'queuemanager.launchpadprojectmilestone': {
299+ 'Meta': {'object_name': 'LaunchpadProjectMilestone', 'db_table': "'launchpad_project_milestones'"},
300+ 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
301+ 'date_targeted': ('django.db.models.fields.DateField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
302+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
303+ 'launchpad_project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.LaunchpadProject']", 'null': 'True', 'blank': 'True'}),
304+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
305+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'})
306+ },
307+ 'queuemanager.lexbuilder': {
308+ 'Meta': {'object_name': 'Lexbuilder', 'db_table': "'lexbuilders'"},
309+ 'contiguous_errors': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
310+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
311+ 'current_job': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.BuildResult']", 'null': 'True', 'db_column': "'current_job_id'", 'blank': 'True'}),
312+ 'current_state': ('django.db.models.fields.CharField', [], {'default': "'UNKNOWN'", 'max_length': '200'}),
313+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
314+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
315+ 'is_okay': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
316+ 'is_retired': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
317+ 'machine_type': ('django.db.models.fields.CharField', [], {'default': "'x86_64'", 'max_length': '200'}),
318+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '200', 'db_index': 'True'}),
319+ 'notes': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
320+ 'previous_state': ('django.db.models.fields.CharField', [], {'default': "'UNKNOWN'", 'max_length': '200'}),
321+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
322+ 'uri': ('django.db.models.fields.CharField', [], {'max_length': '200'})
323+ },
324+ 'queuemanager.project': {
325+ 'Meta': {'object_name': 'Project', 'db_table': "'projects'"},
326+ 'access_groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['django_group_access.AccessGroup']", 'null': 'True', 'blank': 'True'}),
327+ 'arch': ('django.db.models.fields.CharField', [], {'max_length': '10'}),
328+ 'config_url': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
329+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
330+ 'launchpad_project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.LaunchpadProject']", 'null': 'True', 'blank': 'True'}),
331+ 'name': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '200', 'primary_key': 'True', 'db_index': 'True'}),
332+ 'notes': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
333+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'project_owner'", 'null': 'True', 'to': "orm['auth.User']"}),
334+ 'priority': ('django.db.models.fields.IntegerField', [], {'default': '50'}),
335+ 'project_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.ProjectGroup']", 'null': 'True', 'blank': 'True'}),
336+ 'series': ('django.db.models.fields.CharField', [], {'default': "'lucid'", 'max_length': '200'}),
337+ 'status': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True'}),
338+ 'suite': ('django.db.models.fields.CharField', [], {'max_length': '200', 'blank': 'True'}),
339+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '30'})
340+ },
341+ 'queuemanager.projectgroup': {
342+ 'Meta': {'object_name': 'ProjectGroup', 'db_table': "'projectgroups'"},
343+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200', 'primary_key': 'True'}),
344+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '200'})
345+ },
346+ 'queuemanager.projectnotificationsubscription': {
347+ 'Meta': {'object_name': 'ProjectNotificationSubscription', 'db_table': "'project_notification_subscriptions'"},
348+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
349+ 'project': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.Project']", 'db_column': "'project_name'"}),
350+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'db_column': "'user_id'"})
351+ },
352+ 'queuemanager.release': {
353+ 'Meta': {'object_name': 'Release', 'db_table': "'releases'"},
354+ 'build': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['queuemanager.BuildResult']", 'unique': 'True', 'primary_key': 'True'}),
355+ 'checklist_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
356+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
357+ 'creator': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
358+ 'milestone': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['queuemanager.LaunchpadProjectMilestone']", 'null': 'True', 'blank': 'True'}),
359+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '200'}),
360+ 'notes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
361+ 'published_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
362+ 'status': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
363+ 'tag': ('django.db.models.fields.CharField', [], {'max_length': '5', 'null': 'True', 'blank': 'True'}),
364+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
365+ }
366+ }
367+
368+ complete_apps = ['queuemanager']
369
370=== modified file 'lib/offspring/web/queuemanager/models.py'
371--- lib/offspring/web/queuemanager/models.py 2013-05-23 04:18:49 +0000
372+++ lib/offspring/web/queuemanager/models.py 2013-05-24 16:54:37 +0000
373@@ -165,6 +165,7 @@
374 is_active = models.BooleanField(default=True)
375 is_okay = models.BooleanField(default=True, editable=False)
376 is_retired = models.BooleanField(default=False)
377+ contiguous_errors = models.IntegerField(default=0)
378 machine_type = models.CharField(max_length=200, default="x86_64")
379 current_job = models.ForeignKey("BuildResult", db_column="current_job_id", editable=False, blank=True, null=True)
380 notes = models.TextField('whiteboard', blank=True, null=True)

Subscribers

People subscribed via source and target branches