Merge lp:~ursinha/ubuntu-ci-services-itself/ppa-add-private into lp:ubuntu-ci-services-itself

Proposed by Ursula Junque
Status: Merged
Approved by: Ursula Junque
Approved revision: 389
Merged at revision: 385
Proposed branch: lp:~ursinha/ubuntu-ci-services-itself/ppa-add-private
Merge into: lp:ubuntu-ci-services-itself
Diff against target: 287 lines (+93/-31)
7 files modified
juju-deployer/configs/unit_config.yaml.tmpl (+2/-0)
ppa-assigner/ppa_assigner/api.py (+6/-2)
ppa-assigner/ppa_assigner/launchpad.py (+10/-3)
ppa-assigner/ppa_assigner/migrations/0001_initial.py (+2/-0)
ppa-assigner/ppa_assigner/models.py (+23/-13)
ppa-assigner/ppa_assigner/settings.py (+1/-0)
ppa-assigner/ppa_assigner/tests.py (+49/-13)
To merge this branch: bzr merge lp:~ursinha/ubuntu-ci-services-itself/ppa-add-private
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Francis Ginther Approve
Andy Doan (community) Approve
Review via email: mp+210704@code.launchpad.net

Commit message

This branch adds 'private' to PPA, adds ability to get private and public information from launchpad and adds a variable to define we're using private or public ppas.

Description of the change

This branch adds 'private' to PPA. It was supposed to have signing_key_fingerprint, but now it seems all components are allowed to access launchpad so this is not required.

lp_collection now GETs public and private information, and reserve() only gets ppas according to settings.PRIVATE_ONLY.

To post a comment you must log in.
Revision history for this message
Ursula Junque (ursinha) wrote :

I don't like the check on lp_collection because that's lying. It's not fetching private information only, but *also*.

384. By Ursula Junque

Merging trunk

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Andy Doan (doanac) wrote :

45 +def _lp_get(url):

might be better name _lp_get_authenticated

21 - resp = urllib2.urlopen(url)
22 + if settings.PRIVATE_ONLY:
23 + resp = _lp_get(url)
24 + else:
25 + resp = urllib2.urlopen(url)

Is there really any need to distinguish the 2 code paths? Maybe just always call _lp_get there since you already have the logic in the model to differentiate them?

385. By Ursula Junque

s/_lp_get/_lp_get_authenticated/

386. By Ursula Junque

all ppa-assigner/launchpad GETs are now authenticated, bringing also private information

Revision history for this message
Ursula Junque (ursinha) wrote :

> 45 +def _lp_get(url):
>
> might be better name _lp_get_authenticated

r385.

>
> 21 - resp = urllib2.urlopen(url)
> 22 + if settings.PRIVATE_ONLY:
> 23 + resp = _lp_get(url)
> 24 + else:
> 25 + resp = urllib2.urlopen(url)
>
> Is there really any need to distinguish the 2 code paths? Maybe just always
> call _lp_get there since you already have the logic in the model to
> differentiate them?

I think I was only being overcautious about always getting private information (as other methods also use lp_collection), but at this point I think it shouldn't matter really. As I said before I don't even like it, hehe. Fixed on r386.

Do you think these tests are enough? I deployment similar code yesterday and it seemed to me reserve() wasn't respecting the settings.PRIVATE_ONLY variable, and I couldn't find any logs (gunicorn issue, I think). Is there anything else I need to do besides setting that on unit_config? Tests are passing now but maybe I'm not testing this as I should.

Revision history for this message
Ursula Junque (ursinha) wrote :

> Do you think these tests are enough? I deployment similar code yesterday and
> it seemed to me reserve() wasn't respecting the settings.PRIVATE_ONLY
> variable, and I couldn't find any logs (gunicorn issue, I think). Is there
> anything else I need to do besides setting that on unit_config? Tests are
> passing now but maybe I'm not testing this as I should.

s/deployment/deployed/

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Andy Doan (doanac) wrote :

> Do you think these tests are enough? I deployment similar code yesterday and
> it seemed to me reserve() wasn't respecting the settings.PRIVATE_ONLY
> variable, and I couldn't find any logs (gunicorn issue, I think). Is there
> anything else I need to do besides setting that on unit_config? Tests are
> passing now but maybe I'm not testing this as I should.

is it possible you failed to restart django after changing your unit-config and/or settings.py?

139 - qs = PPA.objects.select_for_update().filter(state=PPA.AVAILABLE)
140 + qs = PPA.objects.select_for_update().filter(
141 + state=PPA.AVAILABLE, private=settings.PRIVATE_ONLY)

That looks pretty cut-and-dry to me. I don't think that could do the wrong thing, and:

190 + def testReservePrivateOnly(self):

seems to verify that block

review: Approve
387. By Ursula Junque

Forgot this: engine health page should show total of PPAs available for privacy set

388. By Ursula Junque

Making tests check if engine health will display only private/public ppas

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

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

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

review: Approve (continuous-integration)
Revision history for this message
Francis Ginther (fginther) wrote :

Approve

review: Approve
389. By Ursula Junque

Adding private ppa info to the ui for now, to help us testing

Revision history for this message
Ursula Junque (ursinha) wrote :

I added only the "private ppa only" info to the ui, to help with the confusion enabling private ppas might bring for now.

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

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

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

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju-deployer/configs/unit_config.yaml.tmpl'
2--- juju-deployer/configs/unit_config.yaml.tmpl 2014-03-13 15:28:07 +0000
3+++ juju-deployer/configs/unit_config.yaml.tmpl 2014-03-13 23:30:47 +0000
4@@ -17,6 +17,8 @@
5
6 ppa_pattern: "ci-pool-\\d+"
7
8+private_only: False
9+
10 # for ticket system
11 base_image: "http://cloud-images.ubuntu.com/releases/13.10/release-20131015/ubuntu-13.10-server-cloudimg-amd64-disk1.img"
12 series: saucy
13
14=== modified file 'ppa-assigner/ppa_assigner/api.py'
15--- ppa-assigner/ppa_assigner/api.py 2014-03-12 00:46:19 +0000
16+++ ppa-assigner/ppa_assigner/api.py 2014-03-13 23:30:47 +0000
17@@ -115,10 +115,14 @@
18 status = JSONStatus()
19 status.add_true_false('launchpad configured', lp, lp)
20
21- count = PPA.objects.all().count()
22+ is_private = settings.PRIVATE_ONLY
23+ status.add_true_false('private ppas only', is_private, is_private)
24+
25+ total_ppas = PPA.objects.filter(private=is_private)
26+ count = total_ppas.count()
27 status.add_true_false('total ppas', count, count > 0)
28
29- count = PPA.objects.filter(state=PPA.AVAILABLE).count()
30+ count = total_ppas.filter(state=PPA.AVAILABLE).count()
31 status.add_true_false('available ppas', count, count > 0)
32
33 try:
34
35=== modified file 'ppa-assigner/ppa_assigner/launchpad.py'
36--- ppa-assigner/ppa_assigner/launchpad.py 2014-03-12 00:46:19 +0000
37+++ ppa-assigner/ppa_assigner/launchpad.py 2014-03-13 23:30:47 +0000
38@@ -24,7 +24,7 @@
39
40 def lp_collection(url):
41 '''iterate through each item returned in an lp_collection'''
42- resp = urllib2.urlopen(url)
43+ resp = _lp_get_authenticated(url)
44 resp = resp.read()
45 data = json.loads(resp)
46
47@@ -54,12 +54,19 @@
48 return {
49 'Authorization': ', '.join(values),
50 'Accept': 'application/json',
51- 'Content-Type': 'application/x-www-form-urlencoded'
52 }
53
54
55 def _lp_post(url, data):
56- req = urllib2.Request(url, urllib.urlencode(data), _oauth_headers())
57+ headers = _oauth_headers()
58+ headers['Content-Type'] = 'application/x-www-form-urlencoded'
59+ req = urllib2.Request(url, urllib.urlencode(data), headers)
60+ return urllib2.urlopen(req)
61+
62+
63+def _lp_get_authenticated(url):
64+ '''An authenticated GET to obtain also private information.'''
65+ req = urllib2.Request(url, headers=_oauth_headers())
66 return urllib2.urlopen(req)
67
68
69
70=== modified file 'ppa-assigner/ppa_assigner/migrations/0001_initial.py'
71--- ppa-assigner/ppa_assigner/migrations/0001_initial.py 2014-03-07 22:49:17 +0000
72+++ ppa-assigner/ppa_assigner/migrations/0001_initial.py 2014-03-13 23:30:47 +0000
73@@ -12,6 +12,7 @@
74 db.create_table(u'ppa_assigner_ppa', (
75 (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
76 ('name', self.gf('django.db.models.fields.CharField')(unique=True, max_length=4096)),
77+ ('private', self.gf('django.db.models.fields.BooleanField')(default=False)),
78 ('state', self.gf('django.db.models.fields.PositiveIntegerField')(default=0)),
79 ('ticket_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
80 ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
81@@ -29,6 +30,7 @@
82 'Meta': {'object_name': 'PPA'},
83 u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
84 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '4096'}),
85+ 'private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
86 'state': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
87 'ticket_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
88 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
89
90=== modified file 'ppa-assigner/ppa_assigner/models.py'
91--- ppa-assigner/ppa_assigner/models.py 2014-03-12 00:46:19 +0000
92+++ ppa-assigner/ppa_assigner/models.py 2014-03-13 23:30:47 +0000
93@@ -37,6 +37,7 @@
94 )
95 name = models.CharField(max_length=4096, unique=True)
96
97+ private = models.BooleanField(default=False)
98 state = models.PositiveIntegerField(choices=STATES, default=AVAILABLE)
99 ticket_id = models.PositiveIntegerField()
100 timestamp = models.DateTimeField(auto_now_add=True)
101@@ -58,22 +59,25 @@
102 # note: LP will throttle calls to list PPAs. So you can occassionally
103 # add a PPA, and not see it update for the first few calls to this
104 # method, but it will eventually get in sync
105- model_ppas = set([x.name for x in PPA.objects.all()])
106- add = lp_ppas - model_ppas
107- remove = model_ppas - lp_ppas
108+ lp_ppas_ = set(lp_ppas.keys())
109+ model_ppas = set([ppa.name for ppa in PPA.objects.all()])
110+ add = lp_ppas_ - model_ppas
111+ remove = model_ppas - lp_ppas_
112
113 logging.info('adding new ppas: %r', add)
114- for x in add:
115- PPA.objects.create(name=x, ticket_id=0)
116+ for ppa_name in add:
117+ lp_ppa = lp_ppas[ppa_name]
118+ PPA.objects.create(name=ppa_name, ticket_id=0,
119+ private=lp_ppa['private'])
120
121 logging.info('removing ppas: %r', remove)
122- for x in remove:
123- ppa = PPA.objects.get(name=x)
124+ for ppa_name in remove:
125+ ppa = PPA.objects.get(name=ppa_name)
126 if ppa.state == PPA.RESERVED:
127 # log something, but we may as well delete. because if the
128 # ppa doesn't exist in LP we are already in a catastrophic
129 # situation and shouldn't let any other user re-use this
130- logging.error('ppa %s appears to be in use', x)
131+ logging.error('ppa %s appears to be in use', ppa_name)
132 ppa.delete()
133
134 @staticmethod
135@@ -83,11 +87,16 @@
136 url = url % settings.LAUNCHPAD_PPA_OWNER
137 logging.info('populating ppa model from: %s', url)
138 try:
139+ ppas = {}
140 pat = re.compile(settings.PPA_PATTERN)
141- ppas = ['ppa:%s/%s' % (settings.LAUNCHPAD_PPA_OWNER, x['name'])
142- for x in launchpad.lp_collection(url)
143- if pat.match(x['name'])]
144- PPA._merge_ppas(set(ppas))
145+ for ppa in launchpad.lp_collection(url):
146+ if pat.match(ppa['name']):
147+ ppa_name = 'ppa:{}/{}'.format(settings.LAUNCHPAD_PPA_OWNER,
148+ ppa['name'])
149+ ppas[ppa_name] = {
150+ 'private': ppa['private'],
151+ }
152+ PPA._merge_ppas(ppas)
153 except urllib2.URLError:
154 logging.exception('Could not populate PPAs from launchpad')
155 raise
156@@ -97,7 +106,8 @@
157 # "select_for_update" provides proper db-level locking so we
158 # don't accidentally have 2 concurrent requests find and reserve the
159 # same PPA
160- qs = PPA.objects.select_for_update().filter(state=PPA.AVAILABLE)
161+ qs = PPA.objects.select_for_update().filter(
162+ state=PPA.AVAILABLE, private=settings.PRIVATE_ONLY)
163 if qs.count() == 0:
164 raise PPA.DoesNotExist('No free PPAs exist in pool')
165 pk = qs[0].pk
166
167=== modified file 'ppa-assigner/ppa_assigner/settings.py'
168--- ppa-assigner/ppa_assigner/settings.py 2014-03-12 01:35:01 +0000
169+++ ppa-assigner/ppa_assigner/settings.py 2014-03-13 23:30:47 +0000
170@@ -51,6 +51,7 @@
171 OAUTH_TOKEN_SECRET = _cfg.get('oauth_token_secret', None)
172 OAUTH_REALM = _cfg.get('oauth_realm', 'https://api.launchpad.net/')
173 PPA_PATTERN = _cfg.get('ppa_pattern', r'ci-pool-\d+')
174+PRIVATE_ONLY = _cfg.get('private_only', False)
175
176 DATABASES = {
177 'default': {
178
179=== modified file 'ppa-assigner/ppa_assigner/tests.py'
180--- ppa-assigner/ppa_assigner/tests.py 2014-03-12 00:46:19 +0000
181+++ ppa-assigner/ppa_assigner/tests.py 2014-03-13 23:30:47 +0000
182@@ -39,6 +39,7 @@
183
184 def setUp(self):
185 super(TestModel, self).setUp()
186+ settings.PRIVATE_ONLY = False
187 for n in range(5):
188 name = 'ci_pool-%s' % str(n).zfill(3)
189 PPA.objects.create(name=name, ticket_id=0)
190@@ -57,6 +58,16 @@
191 with self.assertRaises(PPA.DoesNotExist):
192 PPA.reserve(1234)
193
194+ def testReservePrivateOnly(self):
195+ PPA.objects.create(name='ci_pool-998', ticket_id=0)
196+ settings.PRIVATE_ONLY = True
197+ with self.assertRaises(PPA.DoesNotExist):
198+ PPA.reserve(1234)
199+ PPA.objects.create(name='ci_pool-999', ticket_id=0, private=True)
200+ p = PPA.reserve(1234)
201+ self.assertEquals(p.ticket_id, 1234)
202+ self.assertEquals(p.private, True)
203+
204
205 class TestConcurrency(TransactionTestCase):
206 # inspired by, but heavily modified:
207@@ -146,6 +157,19 @@
208 obj = self.createResource('ppa/', params)
209 self.assertEqual(obj['ticket_id'], params['ticket_id'])
210
211+ def testReservePrivateOnly(self):
212+ PPA.objects.create(name='ci-pool_996', ticket_id=0)
213+ params = {'ticket_id': 1234}
214+ # There are only public PPAs available.
215+ settings.PRIVATE_ONLY = True
216+ with self.assertRaisesRegexp(AssertionError, '404 != 201'):
217+ self.post('ppa/', params)
218+
219+ # Creating one private PPA to be reserved.
220+ PPA.objects.create(name='ci-pool_997', ticket_id=0, private=True)
221+ obj = self.createResource('ppa/', params)
222+ self.assertEqual(obj['ticket_id'], params['ticket_id'])
223+
224 def testReserveNoFree(self):
225 PPA.objects.update(state=PPA.DIRTY)
226 params = {'ticket_id': 1234}
227@@ -184,12 +208,13 @@
228 @mock.patch('urllib2.urlopen')
229 def testPopulateMerge(self, urlopen):
230 ignored = [
231- {'name': 'foo'},
232- {'name': 'ci-pool'},
233+ {'name': 'foo', 'private': False},
234+ {'name': 'ci-pool', 'private': False},
235 ]
236 expected = [
237- {'name': 'ci-pool-001'},
238- {'name': 'ci-pool-002'},
239+ {'name': 'ci-pool-001', 'private': False},
240+ {'name': 'ci-pool-002', 'private': True},
241+ {'name': 'ci-pool-003', 'private': False},
242 ]
243 data = {
244 'total_size': 2,
245@@ -201,9 +226,17 @@
246 urlopen.return_value = resp
247 ppa_owner = settings.LAUNCHPAD_PPA_OWNER
248 self.patch('ppa/', {'populate': True})
249- expected = ['ppa:%s/%s' % (ppa_owner, x['name']) for x in expected]
250- found = [x.name for x in PPA.objects.all().order_by('name')]
251- self.assertListEqual(expected, found)
252+ expected = dict(('ppa:{}/{}'.format(ppa_owner, x['name']),
253+ x['private']) for x in expected)
254+ found = dict((x.name, x.private) for x in PPA.objects.all())
255+ self.assertEqual(len(found), len(expected))
256+ self.assertListEqual(sorted(expected.keys()), sorted(found.keys()))
257+ for ppa_name, ppa_privacy in found.iteritems():
258+ self.assertEqual(expected[ppa_name], ppa_privacy)
259+ private_ppa = PPA.objects.filter(private=True)
260+ self.assertEqual(private_ppa.count(), 1)
261+ self.assertEqual(PPA.objects.filter(private=False).count(), 2)
262+ self.assertEqual(PPA.objects.all().count(), 3)
263
264 def testCopyBadParams(self):
265 resource = self._resource('ppa_copy/')
266@@ -227,12 +260,15 @@
267
268 def testStatus(self):
269 status = self.get('status/')
270- total = PPA.objects.all().count()
271- avail = PPA.objects.filter(state=PPA.AVAILABLE).count()
272- self.assertEqual('total ppas', status[1]['label'])
273- self.assertEqual(total, status[1]['value'])
274- self.assertEqual('available ppas', status[2]['label'])
275- self.assertEqual(avail, status[2]['value'])
276+ total_ppas = PPA.objects.filter(private=settings.PRIVATE_ONLY)
277+ total = total_ppas.count()
278+ avail = total_ppas.filter(state=PPA.AVAILABLE).count()
279+ self.assertEqual('private ppas only', status[1]['label'])
280+ self.assertEqual(True, status[1]['value'])
281+ self.assertEqual('total ppas', status[2]['label'])
282+ self.assertEqual(total, status[2]['value'])
283+ self.assertEqual('available ppas', status[3]['label'])
284+ self.assertEqual(avail, status[3]['value'])
285
286
287 class TestCleaner(TestCase):

Subscribers

People subscribed via source and target branches