Merge lp:~cprov/uci-engine/ts-artifact-constraints into lp:uci-engine

Proposed by Celso Providelo
Status: Merged
Approved by: Evan
Approved revision: 832
Merged at revision: 830
Proposed branch: lp:~cprov/uci-engine/ts-artifact-constraints
Merge into: lp:uci-engine
Diff against target: 354 lines (+212/-31)
7 files modified
branch-source-builder/bsbuilder/run_worker.py (+8/-2)
branch-source-builder/bsbuilder/tests/test_worker.py (+62/-0)
juju-deployer/branch-source-builder.yaml.tmpl (+1/-1)
ticket_system/ticket/api.py (+4/-3)
ticket_system/ticket/migrations/0014_auto__chg_field_ticketartifact_ticket__chg_field_subticketartifact_sub.py (+120/-0)
ticket_system/ticket/models.py (+2/-2)
ticket_system/ticket/tests/test_write_api.py (+15/-23)
To merge this branch: bzr merge lp:~cprov/uci-engine/ts-artifact-constraints
Reviewer Review Type Date Requested Status
Ursula Junque (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+237178@code.launchpad.net

Commit message

Fixes TS to prevent orphan artifacts to be stored and the misleading error messages related with processing empty subtickets.

Description of the change

Fixes TS to prevent orphan artifacts to be stored and the misleading error messages related with processing empty subtickets.

Fixes SubTicketArtifact resource in TS, removing a stray (landed by mistake with my ticket-conflicts MP) 'readonly' attribute for 'subticket' FK which leads to empty (no SPU artifacts) subtickets. The underlying problem, in fact, is that artifact FKs to Ticket and SubTicket were nullable for no reason. This MP makes them NOT NULL, since it makes no sense to have a subticket-artifact for no subticket ...

CLI doesn't identify this problem because the TS request succeeds, although subticket.artifacts is empty by the time the ticket is queued. We theoretically could extend CLI checks ...

A further problem is that BSB intermediary steps work, despite of the fact they operate on an empty filelist, up to the point it tries to re-sign the upload(s).

Re-signing upload was failing, just because it has valid keys, but was returning very poor tracing (bare exception on handle_request). This is fixed in this MP too.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:830
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1528/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1528/rebuild

review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:831
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1530/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1530/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ursula Junque (ursinha) wrote :

I have only one note, that we discussed in a hangout:

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :

PASSED: Continuous integration, rev:832
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1542/
Executed test runs:

Click here to trigger a rebuild:
http://s-jenkins.ubuntu-ci:8080/job/uci-engine-ci/1542/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ursula Junque (ursinha) wrote :

Thanks!

review: Approve
Revision history for this message
Ubuntu CI Bot (uci-bot) wrote :

The attempt to merge lp:~cprov/uci-engine/ts-artifact-constraints into lp:uci-engine failed. Below is the output from the failed tests.

ERROR:root:/var/lib/lxc not mounted.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'branch-source-builder/bsbuilder/run_worker.py'
2--- branch-source-builder/bsbuilder/run_worker.py 2014-09-12 13:46:47 +0000
3+++ branch-source-builder/bsbuilder/run_worker.py 2014-10-06 18:30:44 +0000
4@@ -101,9 +101,10 @@
5 # Check that an upload isn't going to fail due to version
6 (failed, subticket_status) = watch_ppa.check_ppa(
7 series, ppa, archive, None, upload_list)
8+ out_data['subticket_status'] = subticket_status
9+
10 if failed:
11 # This is the end of the line, the upload is blocked.
12- out_data['subticket_status'] = subticket_status
13 message = 'Source package upload failed.'
14 log.error(message)
15 out_data['message'] = message
16@@ -132,7 +133,12 @@
17 # In this sense it's not fair to *FAIL* the submitted ticket.
18 # We should find a way to retry the job in another (possibly
19 # saner) instance.
20- return amqp_utils.progress_failed, str(exc)
21+ message = str(exc)
22+ log.error(message)
23+ out_data['message'] = message
24+ out_data['artifacts'].append(
25+ create_result_file(datastore, out_data))
26+ return amqp_utils.progress_failed, out_data
27
28 metric = stats.Metric(
29 'source_package_upload',
30
31=== modified file 'branch-source-builder/bsbuilder/tests/test_worker.py'
32--- branch-source-builder/bsbuilder/tests/test_worker.py 2014-09-12 05:18:35 +0000
33+++ branch-source-builder/bsbuilder/tests/test_worker.py 2014-10-06 18:30:44 +0000
34@@ -192,3 +192,65 @@
35 self.assertEqual(mocked_progress_completed, cb)
36 self.assertSigned('foobar_0.1-1_source.changes')
37 self.assertSigned('foobar_0.1-1.dsc')
38+
39+ @mock.patch('bsbuilder.upload_package.upload_source_packages')
40+ @mock.patch('bsbuilder.upload_package.create_upload_list')
41+ @mock.patch('bsbuilder.upload_package.get_signing_keyring')
42+ @mock.patch('bsbuilder.upload_package.resign_upload')
43+ @mock.patch('ci_utils.amqp_utils.progress_failed')
44+ def test_handle_request_resigning_fails_gracefully(
45+ self, mocked_progress_failed, mocked_keyring, mocked_resign,
46+ mocked_upload_list, mocked_upload):
47+ # `BSBuilderWorker.handle_request` fails gracefully if upload
48+ # re-signing cannot be executed.
49+
50+ # Use a fake upload_list corresponding to the copied testing files.
51+ mocked_upload_list.return_value = [{
52+ 'name': 'foobar',
53+ 'version': '1.0-1',
54+ 'location': self.tmpdir,
55+ 'files': [
56+ 'foobar_0.1-1_source.changes',
57+ 'foobar_0.1-1.dsc',
58+ 'foobar_0.1.orig.tar.gz',
59+ 'foobar_0.1-1.debian.tar.gz'
60+ ],
61+ }]
62+
63+ # Creating successful watch_ppa mocks.
64+ self.check_ppa.return_value = (False, self.fake_subticket_status)
65+ self.watch_ppa.return_value = (0, self.fake_subticket_status)
66+
67+ # Use the testing keyring for signing.
68+ mocked_keyring.return_value = (
69+ TESTING_PRIVATE_KEYRING_DATA,
70+ {'uids': ['A_NAME'], 'fingerprint': 'A_FPR'},
71+ )
72+
73+ # Fail when re-signing uploads.
74+ from bsbuilder import upload_package
75+ mocked_resign.side_effect = [
76+ upload_package.UploadResigningError('I am dead, sorry!')
77+ ]
78+
79+ cb, result = self.worker.handle_request(
80+ self.fake_request_params, self.log)
81+
82+ # BSB fails gracefully, returning tracing information (dict) and
83+ # complete logging stored in swift.
84+ self.assertEqual(mocked_progress_failed, cb)
85+ self.assertEqual(
86+ {'archive': 'an_archive',
87+ 'artifacts': [
88+ {'name': 'package_build.output.log',
89+ 'reference': 'swift_ref',
90+ 'type': 'LOGS'}
91+ ],
92+ 'message': 'I am dead, sorry!',
93+ 'ppa': 'a_ppa',
94+ 'subticket_status': [
95+ {'message': 'SUB_MESSAGE',
96+ 'name': 'the_source',
97+ 'status_text': 'A_STATUS',
98+ 'version': '1.0'}
99+ ]}, result)
100
101=== modified file 'juju-deployer/branch-source-builder.yaml.tmpl'
102--- juju-deployer/branch-source-builder.yaml.tmpl 2014-10-01 11:39:57 +0000
103+++ juju-deployer/branch-source-builder.yaml.tmpl 2014-10-06 18:30:44 +0000
104@@ -9,7 +9,7 @@
105 current_code: ${CI_PAYLOAD_URL}
106 available_code: ${CI_PAYLOAD_URL}
107 unit-config: include-base64://configs/unit_config.yaml
108- packages: "debhelper devscripts dput python-dput python-swiftclient lazr.enum python-txstatsd python-gnupg python-kombu"
109+ packages: "debhelper devscripts dput python-dput python-swiftclient lazr.enum python-txstatsd python-gnupg python-kombu python-mock"
110 install_sources: |
111 - ${CI_PPA}
112 - "cloud:precise-icehouse"
113
114=== modified file 'ticket_system/ticket/api.py'
115--- ticket_system/ticket/api.py 2014-10-02 02:46:09 +0000
116+++ ticket_system/ticket/api.py 2014-10-06 18:30:44 +0000
117@@ -265,7 +265,8 @@
118
119
120 class TicketArtifactResource(ModelResource):
121- ticket = fields.ToOneField(TicketResource, 'ticket', null=True, full=True)
122+ ticket = fields.ToOneField(
123+ TicketResource, 'ticket', full=True)
124
125 class Meta:
126 queryset = TicketArtifact.objects.all()
127@@ -279,8 +280,8 @@
128
129 class SubTicketArtifactResource(ModelResource):
130
131- subticket = fields.ToOneField(SubTicketResource, 'subticket',
132- readonly=True, null=True, full=True)
133+ subticket = fields.ToOneField(
134+ SubTicketResource, 'subticket', full=True)
135
136 class Meta:
137 queryset = SubTicketArtifact.objects.all()
138
139=== added file 'ticket_system/ticket/migrations/0014_auto__chg_field_ticketartifact_ticket__chg_field_subticketartifact_sub.py'
140--- ticket_system/ticket/migrations/0014_auto__chg_field_ticketartifact_ticket__chg_field_subticketartifact_sub.py 1970-01-01 00:00:00 +0000
141+++ ticket_system/ticket/migrations/0014_auto__chg_field_ticketartifact_ticket__chg_field_subticketartifact_sub.py 2014-10-06 18:30:44 +0000
142@@ -0,0 +1,120 @@
143+# -*- coding: utf-8 -*-
144+import datetime
145+from south.db import db
146+from south.v2 import SchemaMigration
147+from django.db import models
148+
149+
150+class Migration(SchemaMigration):
151+
152+ def forwards(self, orm):
153+
154+ # Changing field 'TicketArtifact.ticket'
155+ db.alter_column('ticket_artifact', 'ticket_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ticket.Ticket'], null=False))
156+
157+ # Changing field 'SubTicketArtifact.subticket'
158+ db.alter_column('subticket_artifact', 'subticket_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ticket.SubTicket'], null=False))
159+
160+ def backwards(self, orm):
161+
162+ # Changing field 'TicketArtifact.ticket'
163+ db.alter_column('ticket_artifact', 'ticket_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ticket.Ticket'], null=True))
164+
165+ # Changing field 'SubTicketArtifact.subticket'
166+ db.alter_column('subticket_artifact', 'subticket_id', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['ticket.SubTicket'], null=True))
167+
168+ models = {
169+ u'project.sourcepackage': {
170+ 'Meta': {'object_name': 'SourcePackage', 'db_table': "'sourcepackage'"},
171+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
172+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '4096'})
173+ },
174+ u'ticket.mergeproposal': {
175+ 'Meta': {'object_name': 'MergeProposal', 'db_table': "'mergeproposal'"},
176+ 'approved_revno': ('django.db.models.fields.IntegerField', [], {}),
177+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
178+ 'lp_url': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
179+ 'project': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
180+ 'sourcepackage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['project.SourcePackage']"}),
181+ 'target_branch': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
182+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
183+ },
184+ u'ticket.review': {
185+ 'Meta': {'object_name': 'Review'},
186+ 'completed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
187+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
188+ 'review_type': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
189+ 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ticket.Ticket']"}),
190+ 'workflow_step': ('django.db.models.fields.IntegerField', [], {})
191+ },
192+ u'ticket.sourcepackageupload': {
193+ 'Meta': {'object_name': 'SourcePackageUpload', 'db_table': "'sourcepackageupload'"},
194+ 'architecture': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
195+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
196+ 'sourcepackage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['project.SourcePackage']"}),
197+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
198+ },
199+ u'ticket.subticket': {
200+ 'Meta': {'ordering': "['id']", 'object_name': 'SubTicket', 'db_table': "'subticket'"},
201+ 'assignee': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
202+ 'current_workflow_step': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '4096'}),
203+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
204+ 'merge_proposal': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['ticket.MergeProposal']", 'null': 'True', 'blank': 'True'}),
205+ 'source_package_upload': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': u"orm['ticket.SourcePackageUpload']", 'null': 'True', 'blank': 'True'}),
206+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '4096'}),
207+ 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ticket.Ticket']"})
208+ },
209+ u'ticket.subticketartifact': {
210+ 'Meta': {'object_name': 'SubTicketArtifact', 'db_table': "'subticket_artifact'"},
211+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
212+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
213+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
214+ 'subticket': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ticket.SubTicket']"}),
215+ 'type': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
216+ },
217+ u'ticket.ticket': {
218+ 'Meta': {'ordering': "['id']", 'object_name': 'Ticket', 'db_table': "'ticket'"},
219+ 'base_image': ('django.db.models.fields.CharField', [], {'default': "'http://cloud-images.ubuntu.com/utopic/current/utopic-server-cloudimg-amd64-disk1.img'", 'max_length': '4096'}),
220+ 'bug_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
221+ 'build_ppa': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'blank': 'True'}),
222+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
223+ 'creator': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'null': 'True', 'blank': 'True'}),
224+ 'current_workflow_step': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '4096'}),
225+ 'description': ('django.db.models.fields.TextField', [], {}),
226+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
227+ 'lander_unit': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
228+ 'master_ppa': ('django.db.models.fields.CharField', [], {'default': "'ppa:cprov/ci-master'", 'max_length': '4096'}),
229+ 'owner': ('django.db.models.fields.EmailField', [], {'max_length': '254'}),
230+ 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
231+ 'series': ('django.db.models.fields.CharField', [], {'default': "'utopic'", 'max_length': '4096'}),
232+ 'status': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '4096'}),
233+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
234+ 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
235+ 'uuid': ('django.db.models.fields.CharField', [], {'default': "'785afdfa-4c2a-11e4-9446-001c4229f9ac'", 'unique': 'True', 'max_length': '36'}),
236+ 'workflow': ('django.db.models.fields.related.ForeignKey', [], {'default': "'default'", 'to': u"orm['ticket.Workflow']"})
237+ },
238+ u'ticket.ticketartifact': {
239+ 'Meta': {'object_name': 'TicketArtifact', 'db_table': "'ticket_artifact'"},
240+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
242+ 'reference': ('django.db.models.fields.CharField', [], {'max_length': '4096'}),
243+ 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ticket.Ticket']"}),
244+ 'type': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
245+ },
246+ u'ticket.ticketcitrainoverlay': {
247+ 'Meta': {'object_name': 'TicketCiTrainOverlay', 'db_table': "'ticketcitrainoverlay'"},
248+ 'comments': ('django.db.models.fields.TextField', [], {}),
249+ 'job_url': ('django.db.models.fields.URLField', [], {'max_length': '200'}),
250+ 'landers': ('django.db.models.fields.TextField', [], {}),
251+ 'sync_request': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
252+ 'test_notes': ('django.db.models.fields.TextField', [], {}),
253+ 'ticket': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'citrain_overlay'", 'unique': 'True', 'primary_key': 'True', 'to': u"orm['ticket.Ticket']"})
254+ },
255+ u'ticket.workflow': {
256+ 'Meta': {'object_name': 'Workflow'},
257+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '4096', 'primary_key': 'True'}),
258+ 'steps': ('django.db.models.fields.CharField', [], {'max_length': '4096'})
259+ }
260+ }
261+
262+ complete_apps = ['ticket']
263
264=== modified file 'ticket_system/ticket/models.py'
265--- ticket_system/ticket/models.py 2014-10-02 20:04:57 +0000
266+++ ticket_system/ticket/models.py 2014-10-06 18:30:44 +0000
267@@ -351,7 +351,7 @@
268 db_table = 'subticket_artifact'
269
270 type = models.CharField(choices=SUBTICKET_ARTIFACT_TYPE, max_length=4096)
271- subticket = models.ForeignKey(SubTicket, null=True)
272+ subticket = models.ForeignKey(SubTicket)
273
274 def __unicode__(self):
275 return "{} {}".format(self.name, self.type)
276@@ -368,7 +368,7 @@
277 db_table = 'ticket_artifact'
278
279 type = models.CharField(choices=TICKET_ARTIFACT_TYPE, max_length=4096)
280- ticket = models.ForeignKey(Ticket, null=True)
281+ ticket = models.ForeignKey(Ticket)
282
283 def __unicode__(self):
284 return "{} {}".format(self.name, self.type)
285
286=== modified file 'ticket_system/ticket/tests/test_write_api.py'
287--- ticket_system/ticket/tests/test_write_api.py 2014-10-02 20:04:57 +0000
288+++ ticket_system/ticket/tests/test_write_api.py 2014-10-06 18:30:44 +0000
289@@ -32,7 +32,6 @@
290 Review,
291 SourcePackageUpload,
292 SubTicket,
293- SubTicketArtifact,
294 SubTicketWorkflowStep,
295 SubTicketWorkflowStepStatus,
296 Ticket,
297@@ -357,42 +356,35 @@
298 self.subticket = mommy.make('SubTicket', ticket=self.ticket,
299 source_package_upload=self.spu)
300 self.artifact = mommy.make('SubTicketArtifact',
301+ name='existing_artifact',
302 subticket=self.subticket)
303 self.resource = 'subticketartifact/'
304 self.detail_url = self.resource + '{0}/'.format(self.artifact.pk)
305 self.subticket_uri = '/api/v1/subticket/{0}/'.format(self.subticket.pk)
306 self.post_artifact_data_subticket = {
307 'type': 'SPU',
308- 'name': 'my_artifact',
309+ 'name': 'new_artifact',
310 'subticket': self.subticket_uri,
311 'reference': 'http://www.example.com/m1CTLg5FHY',
312 }
313
314 def test_post_artifact_subticket(self):
315- """
316- Test creating an artifact for a subticket via the API
317- """
318- # Check how many are there first.
319- self.assertEqual(SubTicketArtifact.objects.count(), 1)
320+ # `SubTicketArtifact` resource can be created via the API.
321 self.post(resource=self.resource,
322 params=self.post_artifact_data_subticket)
323- # Verify a new one has been added.
324- self.assertEqual(SubTicketArtifact.objects.count(), 2)
325-
326- def test_patch_detail(self):
327- # Grab the current data & modify it slightly.
328- original_data = self.getResource(self.detail_url)
329- new_data = original_data.copy()
330- new_data['name'] = 'my_upload'
331-
332- self.assertEqual(SubTicketArtifact.objects.count(), 1)
333- resp = self.client.patch('/api/v1/' + self.detail_url, data=new_data)
334+ # Retrieve `SubTicketArtifact`s via their context `SubTicket`,
335+ # this way we can be sure they are properly related.
336+ [existing, new] = list(self.subticket.subticketartifact_set.all())
337+ self.assertEqual(existing, self.artifact)
338+ self.assertEqual('SPU', new.type)
339+ self.assertEqual('new_artifact', new.name)
340+ self.assertEqual('http://www.example.com/m1CTLg5FHY', new.reference)
341+
342+ def test_patch_artifact_not_allowed(self):
343+ # `SubTicketArtifact` resource cannot be patched via the API.
344+ resp = self.client.patch(
345+ '/api/v1/' + self.detail_url, data={'name': 'modified_artifact'})
346 self.assertHttpMethodNotAllowed(resp)
347- # Make sure the count hasn't changed & we did an update.
348- self.assertEqual(SubTicketArtifact.objects.count(), 1)
349- # Check for updated data.
350- self.assertEqual(SubTicketArtifact.objects.get(
351- pk=self.artifact.pk).name, self.artifact.name)
352
353
354 class APIUpdateTicketResourceTest(TicketTastypieTestCase):

Subscribers

People subscribed via source and target branches