Merge lp:~doanac/ubuntu-ci-services-itself/ppa-cleaner into lp:ubuntu-ci-services-itself

Proposed by Andy Doan
Status: Merged
Approved by: Andy Doan
Approved revision: no longer in the source branch.
Merged at revision: 47
Proposed branch: lp:~doanac/ubuntu-ci-services-itself/ppa-cleaner
Merge into: lp:ubuntu-ci-services-itself
Diff against target: 680 lines (+349/-129)
8 files modified
ppa-assigner/local_settings.py.example (+5/-0)
ppa-assigner/ppa_assigner/api.py (+21/-7)
ppa-assigner/ppa_assigner/launchpad.py (+87/-0)
ppa-assigner/ppa_assigner/management/commands/clean_ppas.py (+79/-0)
ppa-assigner/ppa_assigner/models.py (+34/-51)
ppa-assigner/ppa_assigner/settings.py (+6/-0)
ppa-assigner/ppa_assigner/tests.py (+116/-71)
ppa-assigner/setup.py (+1/-0)
To merge this branch: bzr merge lp:~doanac/ubuntu-ci-services-itself/ppa-cleaner
Reviewer Review Type Date Requested Status
Andy Doan (community) Approve
Vincent Ladeuil (community) Needs Fixing
Review via email: mp+198637@code.launchpad.net

Commit message

Add ppa-cleaning management command to the ppa-assigner

Description of the change

This adds ppa-cleaning logic to the PPA-assigner via a django management command. In the future, we'll update our charm so that this command is run via upstart.

The main work was done in ppa-assigner/ppa_assigner/management/commands/clean_ppas.py. I refactored a little bit of code from models.py into a new launchpad.py utility.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

21 +# Read for details: https://help.launchpad.net/API/SigningRequests
22 +OAUTH_CONSUMER_KEY = 'TODO'
23 +OAUTH_TOKEN = 'TODO'
24 +OAUTH_TOKEN_SECRET = 'TODO'
25 +

Not sure how this relates to your other MP, I'll consider you've addressed the TODO there for this review.

60 - lockname = bundle.data.get('lockname')

\o/ no more lock ;)

79 + if type(state) != int:

O_o can you add a comment to explain ? As I read it it means we'll be ignoring some requests. I'd rather error out unless you have a strong reason to do so... which will make a perfect comment ;)

118 +def lp_collection(url):

You probably want to talk to wgrant about that. If I remember correctly, if the collection is big/huge, you'll run into timeouts and will need to make several requests to acquire parts of the collection.

So better check with wgrant first if you're under a reasonable size for the collection or if you already need to handle chunks.

138 + 'OAuth realm="https://api.launchpad.net/"',
139 + 'oauth_version="1.0"',
140 + 'oauth_consumer_key="%s"' % settings.OAUTH_CONSUMER_KEY,
141 + 'oauth_token="%s"' % settings.OAUTH_TOKEN,
142 + 'oauth_signature_method="PLAINTEXT"',
143 + 'oauth_signature="%%26%s"' % settings.OAUTH_TOKEN_SECRET,
144 + 'oauth_timestamp="%d"' % int(oauth.time.time()),
145 + 'oauth_nonce="%s"' % oauth.generate_nonce(),

Great, we already have 3 settings parametrized, I think we also want OAuth realm so we can test against staging or a local launchpad.

161 + url = 'https://api.launchpad.net/1.0/~%s/+archive/%s/?ws.op='

That one probably needs to be parametrized in the same way.

223 + # TODO check for oauth before launching

Sounds like a good test to add ;)

249 + logging.exception('unable to request clean ppa, will retry')

Better add the exception encountered if only to start learning about which ones exist.

266 + logging.exception('error checking if ppa is clean')

idem

354 - # TODO test if this is needed or if there is some type
355 - # of looping logic needed

Yeah, see above, not sure you're talking about the same looping logic but that matches what I'm talking about ;)

364 logging.exception('Could not populate PPAs from launchpad')

Again, mention the exception, the logs are for us and we want precise info ;)

By just reading the proposal, this seems better than the previous version, so +once the tweaks above are addressed ;)

review: Needs Fixing
Revision history for this message
Andy Doan (doanac) wrote :
Download full text (3.4 KiB)

On 12/16/2013 07:34 AM, Vincent Ladeuil wrote:
> Review: Needs Fixing
>
> 21 +# Read for details: https://help.launchpad.net/API/SigningRequests
> 22 +OAUTH_CONSUMER_KEY = 'TODO'
> 23 +OAUTH_TOKEN = 'TODO'
> 24 +OAUTH_TOKEN_SECRET = 'TODO'
> 25 +
>
> Not sure how this relates to your other MP, I'll consider you've addressed the TODO there for this review.

TODO's are filler. Our juju-deployer configs will override this during
deployment.

> 60 - lockname = bundle.data.get('lockname')
>
> \o/ no more lock ;)

Good catch, revno 25

> 79 + if type(state) != int:
>
> O_o can you add a comment to explain ? As I read it it means we'll be ignoring some requests. I'd rather error out unless you have a strong reason to do so... which will make a perfect comment ;)

good catch, revno 26

> 118 +def lp_collection(url):
>
> You probably want to talk to wgrant about that. If I remember correctly, if the collection is big/huge, you'll run into timeouts and will need to make several requests to acquire parts of the collection.
>
> So better check with wgrant first if you're under a reasonable size for the collection or if you already need to handle chunks.

No issue here. Launchpad returns these back in chunks of items. This
function deal with grabbing those chunks and returning them back
one-by-one so that we don't get overloaded.

> 138 + 'OAuth realm="https://api.launchpad.net/"',
> 139 + 'oauth_version="1.0"',
> 140 + 'oauth_consumer_key="%s"' % settings.OAUTH_CONSUMER_KEY,
> 141 + 'oauth_token="%s"' % settings.OAUTH_TOKEN,
> 142 + 'oauth_signature_method="PLAINTEXT"',
> 143 + 'oauth_signature="%%26%s"' % settings.OAUTH_TOKEN_SECRET,
> 144 + 'oauth_timestamp="%d"' % int(oauth.time.time()),
> 145 + 'oauth_nonce="%s"' % oauth.generate_nonce(),
>
> Great, we already have 3 settings parametrized, I think we also want OAuth realm so we can test against staging or a local launchpad.

revno 27

> 161 + url = 'https://api.launchpad.net/1.0/~%s/+archive/%s/?ws.op='
>
> That one probably needs to be parametrized in the same way.

revno: 28

>
> 249 + logging.exception('unable to request clean ppa, will retry')
>
> Better add the exception encountered if only to start learning about which ones exist.
>
> 266 + logging.exception('error checking if ppa is clean')
>

good catch - revno 29

>
> 354 - # TODO test if this is needed or if there is some type
> 355 - # of looping logic needed
>
> Yeah, see above, not sure you're talking about the same looping logic but that matches what I'm talking about ;)

as noted. this MP actually makes this stuff work properly.

> 364 logging.exception('Could not populate PPAs from launchpad')
>
> Again, mention the exception, the logs are for us and we want precise info ;)

no - good catch. i was thinking logging.exception automatically included
the stack trace.

However - in this case i'm re-raising the exception. So I thought the
caller should log the exception. I was concerned both people would log
the stack trace and it would make it 2x longer to read than needed.

I'll go ahead and log it here, because I don't think my rationale is
strong enough :)

> By just reading the proposal, this seems better than the previous versi...

Read more...

Revision history for this message
Chris Johnston (cjohnston) wrote :

Attempt to merge into lp:ubuntu-ci-services-itself failed due to conflicts:

text conflict in juju-deployer/ppa-assigner.yaml

Revision history for this message
Chris Johnston (cjohnston) wrote :

Voting does not meet specified criteria. Required: Approve >= 1. Got: 1 Needs Fixing.

Revision history for this message
Andy Doan (doanac) wrote :

+1 based on vila's review comment

review: Approve
47. By Andy Doan

[r=Andy Doan] Add ppa-cleaning management command to the ppa-assigner from Andy Doan

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ppa-assigner/local_settings.py.example'
2--- ppa-assigner/local_settings.py.example 2013-11-26 20:55:48 +0000
3+++ ppa-assigner/local_settings.py.example 2013-12-18 18:24:41 +0000
4@@ -19,6 +19,11 @@
5
6 LAUNCHPAD_PPA_USER = 'TODO'
7
8+# Read for details: https://help.launchpad.net/API/SigningRequests
9+OAUTH_CONSUMER_KEY = 'TODO'
10+OAUTH_TOKEN = 'TODO'
11+OAUTH_TOKEN_SECRET = 'TODO'
12+
13 BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
14
15 DATABASES = {
16
17=== modified file 'ppa-assigner/ppa_assigner/api.py'
18--- ppa-assigner/ppa_assigner/api.py 2013-11-22 17:47:12 +0000
19+++ ppa-assigner/ppa_assigner/api.py 2013-12-18 18:24:41 +0000
20@@ -13,11 +13,13 @@
21 # You should have received a copy of the GNU Affero General Public License
22 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23
24+import urllib2
25+
26 from tastypie.api import Api
27 from tastypie.http import HttpAccepted, HttpApplicationError
28 from tastypie.resources import ALL
29
30-from ppa_assigner.models import PPA, LaunchpadError
31+from ppa_assigner.models import PPA
32
33 from ci_utils.tastypie.resource import AuthorizedResource
34
35@@ -28,15 +30,15 @@
36 resource_name = 'ppa'
37
38 filtering = {
39- 'reserved': ALL,
40- 'lockname': ALL,
41+ 'state': ALL,
42+ 'ticket_id': ALL,
43+ 'timestamp': ALL,
44 }
45
46 def obj_create(self, bundle, **kwargs):
47 '''We don't really create an object, so override tastypie.'''
48- lockname = bundle.data.get('lockname')
49- clean = bundle.data.get('clean')
50- bundle.obj = PPA.reserve(lockname, clean)
51+ ticket_id = bundle.data.get('ticket_id')
52+ bundle.obj = PPA.reserve(ticket_id)
53 return bundle
54
55 def patch_list(self, request, **kwargs):
56@@ -45,10 +47,22 @@
57 # populate is a bit different than normal "patch" operations
58 PPA.populate_from_launchpad()
59 return HttpAccepted()
60- except LaunchpadError as e:
61+ except urllib2.URLError as e:
62 return HttpApplicationError(e)
63 return super(PPAResource, self).patch_list(request, **kwargs)
64
65+ def hydrate_state(self, bundle):
66+ state = bundle.data['state']
67+ # tastypie by default will only let us refer to the state by their
68+ # numeric value. This is awkward for users, so this lets a user
69+ # specify something like "dirty" rather than 20.
70+ if type(state) != int:
71+ for val, label in PPA.STATES:
72+ if state == label:
73+ bundle.data['state'] = val
74+ break
75+ return bundle
76+
77
78 v1_api = Api(api_name='v1')
79 v1_api.register(PPAResource())
80
81=== added file 'ppa-assigner/ppa_assigner/launchpad.py'
82--- ppa-assigner/ppa_assigner/launchpad.py 1970-01-01 00:00:00 +0000
83+++ ppa-assigner/ppa_assigner/launchpad.py 2013-12-18 18:24:41 +0000
84@@ -0,0 +1,87 @@
85+# Ubuntu CI Services
86+# Copyright 2012-2013 Canonical Ltd.
87+
88+# This program is free software: you can redistribute it and/or modify it
89+# under the terms of the GNU Affero General Public License version 3, as
90+# published by the Free Software Foundation.
91+
92+# This program is distributed in the hope that it will be useful, but
93+# WITHOUT ANY WARRANTY; without even the implied warranties of
94+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
95+# PURPOSE. See the GNU Affero General Public License for more details.
96+
97+# You should have received a copy of the GNU Affero General Public License
98+# along with this program. If not, see <http://www.gnu.org/licenses/>.
99+
100+import json
101+import logging
102+import urllib
103+import urllib2
104+
105+from django.conf import settings
106+from oauth import oauth
107+
108+
109+def lp_collection(url):
110+ '''iterate through each item returned in an lp_collection'''
111+ resp = urllib2.urlopen(url)
112+ resp = resp.read()
113+ data = json.loads(resp)
114+
115+ for d in data['entries']:
116+ yield d
117+
118+ if 'next_collection_link' in data:
119+ for d in lp_collection(data['next_collection_link']):
120+ yield d
121+
122+
123+def _oauth_headers():
124+ '''oauth header required for a launchpad api post
125+
126+ https://help.launchpad.net/API/SigningRequests
127+ '''
128+ values = [
129+ 'OAuth realm="%s"' % settings.OAUTH_REALM,
130+ 'oauth_version="1.0"',
131+ 'oauth_consumer_key="%s"' % settings.OAUTH_CONSUMER_KEY,
132+ 'oauth_token="%s"' % settings.OAUTH_TOKEN,
133+ 'oauth_signature_method="PLAINTEXT"',
134+ 'oauth_signature="%%26%s"' % settings.OAUTH_TOKEN_SECRET,
135+ 'oauth_timestamp="%d"' % int(oauth.time.time()),
136+ 'oauth_nonce="%s"' % oauth.generate_nonce(),
137+ ]
138+ return {
139+ 'Authorization': ', '.join(values),
140+ 'Accept': 'application/json',
141+ 'Content-Type': 'application/x-www-form-urlencoded'
142+ }
143+
144+
145+def _lp_post(url, data):
146+ req = urllib2.Request(url, urllib.urlencode(data), _oauth_headers())
147+ return urllib2.urlopen(req)
148+
149+
150+def get_publishing_history(ppa):
151+ '''A generator for the source and binary publish history of a ppa.'''
152+ url = settings.LAUNCHPAD_API_BASE + '/~%s/+archive/%s/?ws.op='
153+ url = url % (settings.LAUNCHPAD_PPA_USER, ppa.split('/')[-1])
154+
155+ for x in lp_collection(url + 'getPublishedSources'):
156+ yield x
157+ for x in lp_collection(url + 'getPublishedBinaries'):
158+ yield x
159+
160+
161+def request_ppa_clean(ppa):
162+ '''call requestDeletion operation on ppa's source and binary pub history'''
163+ for x in get_publishing_history(ppa):
164+ if x['date_removed']:
165+ continue
166+ logging.debug('requesting deletion of: %s', x['self_link'])
167+ resp = _lp_post(x['self_link'], {'ws.op': 'requestDeletion'})
168+ code = resp.getcode()
169+ if code != 200:
170+ raise urllib2.HTTPError(
171+ x['self_link'], code, resp.read(), resp.headers.headers, None)
172
173=== added directory 'ppa-assigner/ppa_assigner/management'
174=== added file 'ppa-assigner/ppa_assigner/management/__init__.py'
175=== added directory 'ppa-assigner/ppa_assigner/management/commands'
176=== added file 'ppa-assigner/ppa_assigner/management/commands/__init__.py'
177=== added file 'ppa-assigner/ppa_assigner/management/commands/clean_ppas.py'
178--- ppa-assigner/ppa_assigner/management/commands/clean_ppas.py 1970-01-01 00:00:00 +0000
179+++ ppa-assigner/ppa_assigner/management/commands/clean_ppas.py 2013-12-18 18:24:41 +0000
180@@ -0,0 +1,79 @@
181+# Ubuntu CI Services
182+# Copyright 2012-2013 Canonical Ltd.
183+
184+# This program is free software: you can redistribute it and/or modify it
185+# under the terms of the GNU Affero General Public License version 3, as
186+# published by the Free Software Foundation.
187+
188+# This program is distributed in the hope that it will be useful, but
189+# WITHOUT ANY WARRANTY; without even the implied warranties of
190+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
191+# PURPOSE. See the GNU Affero General Public License for more details.
192+
193+# You should have received a copy of the GNU Affero General Public License
194+# along with this program. If not, see <http://www.gnu.org/licenses/>.
195+
196+import logging
197+import time
198+
199+from optparse import make_option
200+
201+from django.core.management.base import BaseCommand
202+
203+from ppa_assigner import launchpad
204+from ppa_assigner.models import PPA
205+
206+
207+class Command(BaseCommand):
208+ help = 'Runs forever to take dirty PPAs and get them ready for re-use.'
209+ option_list = BaseCommand.option_list + (
210+ make_option('--interval', dest='interval', default=10, type=int,
211+ help='Interval to sleep between checks.'),
212+ )
213+
214+ # TODO check for oauth before launching
215+
216+ def handle(self, *args, **options):
217+ level = logging.INFO
218+ if int(options['verbosity']) > 1:
219+ level = logging.DEBUG
220+ logging.basicConfig(level=level)
221+
222+ interval = options['interval']
223+ try:
224+ while True:
225+ self.dirty_to_cleaning()
226+ self.cleaning_to_available()
227+ time.sleep(interval)
228+ except KeyboardInterrupt:
229+ print('command cancelled by user')
230+
231+ def dirty_to_cleaning(self):
232+ logging.debug('Check for PPAs that need to be cleaned...')
233+ qs = PPA.objects.select_for_update().filter(state=PPA.DIRTY)
234+ if qs.count():
235+ logging.info('Requesting launchpad clean: %s', qs[0].name)
236+ try:
237+ launchpad.request_ppa_clean(qs[0].name)
238+ qs.filter(pk__in=(qs[0].pk,)).update(state=PPA.CLEANING)
239+ except Exception as e:
240+ logging.exception('unable to request clean ppa, will retry', e)
241+
242+ def cleaning_to_available(self):
243+ logging.debug('Check for PPAs that are avialable...')
244+ qs = PPA.objects.select_for_update().filter(state=PPA.CLEANING)
245+ if qs.count():
246+ if self._lp_is_clean(qs[0]):
247+ logging.info('ppa is available for use: %s', qs[0].name)
248+ qs.filter(pk__in=(qs[0].pk,)).update(state=PPA.AVAILABLE)
249+
250+ def _lp_is_clean(self, ppa):
251+ logging.debug('Checking if ppa is ready: %s', ppa.name)
252+ try:
253+ for x in launchpad.get_publishing_history(ppa.name):
254+ if not x['date_removed']:
255+ return False
256+ except Exception as e:
257+ logging.exception('error checking if ppa is clean', e)
258+ return False
259+ return True
260
261=== modified file 'ppa-assigner/ppa_assigner/models.py'
262--- ppa-assigner/ppa_assigner/models.py 2013-11-26 20:55:48 +0000
263+++ ppa-assigner/ppa_assigner/models.py 2013-12-18 18:24:41 +0000
264@@ -14,36 +14,43 @@
265 # along with this program. If not, see <http://www.gnu.org/licenses/>.
266
267 import datetime
268-import json
269 import logging
270 import urllib2
271
272 from django.conf import settings
273 from django.db import models
274
275-
276-class LaunchpadError(Exception):
277- '''Raised when an error occurrs interacting with Launchpad.'''
278+from ppa_assigner import launchpad
279
280
281 class PPA(models.Model):
282+ AVAILABLE = 0
283+ RESERVED = 10
284+ DIRTY = 20
285+ CLEANING = 30
286+ STATES = (
287+ (AVAILABLE, 'available'),
288+ (RESERVED, 'reserved'),
289+ (DIRTY, 'dirty'),
290+ (CLEANING, 'cleaning'),
291+ )
292 name = models.CharField(max_length=4096, unique=True)
293
294- reserved = models.BooleanField(default=False)
295- lockname = models.CharField(max_length=4096, blank=True)
296+ state = models.PositiveIntegerField(choices=STATES, default=AVAILABLE)
297+ ticket_id = models.PositiveIntegerField()
298 timestamp = models.DateTimeField(auto_now_add=True)
299
300 def __unicode__(self):
301- reserved = ''
302- if self.reserved:
303- reserved = ' (reserved)'
304- if self.lockname:
305- reserved = '%s - %s' % (reserved, self.lockname)
306- return '%s %s' % (self.name, reserved)
307+ val = '%s (%s)' % (self.name, self._get_state_str())
308+ if self.state == PPA.RESERVED:
309+ val += ' ticket=%s' % self.ticket_id
310+ return val
311
312- def clean_artifacts(self):
313- """Cleans up all the artifacts residing in the ppa's."""
314- raise LaunchpadError('TODO how to clean via LP')
315+ def _get_state_str(self):
316+ for state, label in PPA.STATES:
317+ if self.state == state:
318+ return label
319+ raise RuntimeError('Unsupported state: %s' % self.state)
320
321 @staticmethod
322 def _merge_ppas(lp_ppas):
323@@ -53,12 +60,12 @@
324
325 logging.info('adding new ppas: %r', add)
326 for x in add:
327- PPA.objects.create(name=x)
328+ PPA.objects.create(name=x, ticket_id=0)
329
330 logging.info('removing ppas: %r', add)
331 for x in remove:
332 ppa = PPA.objects.get(name=x)
333- if ppa.reserved:
334+ if ppa.state == PPA.RESERVED:
335 # log something, but we may as well delete. because if the
336 # ppa doesn't exist in LP we are already in a catastrophic
337 # situation and shouldn't let any other user re-use this
338@@ -68,51 +75,27 @@
339 @staticmethod
340 def populate_from_launchpad():
341 '''Get list of PPAs from launchpad and ensure they exist here.'''
342- url = 'https://api.launchpad.net/1.0/~%s/ppas'
343+ url = settings.LAUNCHPAD_API_BASE + '/~%s/ppas'
344 url = url % settings.LAUNCHPAD_PPA_USER
345 logging.info('populating ppa model from: %s', url)
346 try:
347- resp = urllib2.urlopen(url)
348- resp = resp.read()
349- data = json.loads(resp)
350- # TODO test if this is needed or if there is some type
351- # of looping logic needed
352- assert data['total_size'] == len(data['entries'])
353 ppas = ['ppa:%s/%s' % (settings.LAUNCHPAD_PPA_USER, x['name'])
354- for x in data['entries']]
355+ for x in launchpad.lp_collection(url)]
356 PPA._merge_ppas(set(ppas))
357-
358 except urllib2.URLError as e:
359- logging.exception('Could not populate PPAs from launchpad')
360- raise LaunchpadError(e)
361+ logging.exception('Could not populate PPAs from launchpad', e)
362+ raise
363
364 @staticmethod
365- def reserve(lockname='', clean=False):
366+ def reserve(ticket_id):
367 # "select_for_update" provides proper db-level locking so we
368 # don't accidentally have 2 concurrent requests find and reserve the
369 # same PPA
370- ppa = None
371- qs = PPA.objects.select_for_update().filter(reserved=False)
372+ qs = PPA.objects.select_for_update().filter(state=PPA.AVAILABLE)
373 if qs.count() == 0:
374 raise PPA.DoesNotExist('No free PPAs exist in pool')
375 pk = qs[0].pk
376- qs.filter(pk__in=(pk,)).update(reserved=True, lockname=lockname)
377-
378- # we now hold a lock, be careful
379- try:
380- ppa = PPA.objects.get(pk=pk)
381- ppa.timestamp = datetime.datetime.now()
382- if clean:
383- ppa.clean_artifacts()
384-
385- ppa.save()
386- return ppa
387- except Exception as e:
388- if ppa:
389- ppa.reserved = False
390- ppa.lockname = ''
391- ppa.save()
392- if not isinstance(e, LaunchpadError):
393- logging.exception('Unexpected exception while cleaning')
394- e = LaunchpadError(e)
395- raise e
396+ now = datetime.datetime.utcnow()
397+ qs.filter(pk__in=(pk,)).update(
398+ state=PPA.RESERVED, ticket_id=ticket_id, timestamp=now)
399+ return PPA.objects.get(id=pk)
400
401=== modified file 'ppa-assigner/ppa_assigner/settings.py'
402--- ppa-assigner/ppa_assigner/settings.py 2013-12-12 23:22:27 +0000
403+++ ppa-assigner/ppa_assigner/settings.py 2013-12-18 18:24:41 +0000
404@@ -25,6 +25,12 @@
405 MANAGERS = ADMINS
406
407 LAUNCHPAD_PPA_USER = 'TODO'
408+LAUNCHPAD_API_BASE = 'https://api.launchpad.net/1.0'
409+
410+OAUTH_REALM = 'https://api.launchpad.net/'
411+OAUTH_CONSUMER_KEY = 'TODO'
412+OAUTH_TOKEN = 'TODO'
413+OAUTH_TOKEN_SECRET = 'TODO'
414
415 DATABASES = {
416 'default': {
417
418=== modified file 'ppa-assigner/ppa_assigner/tests.py'
419--- ppa-assigner/ppa_assigner/tests.py 2013-12-10 18:05:51 +0000
420+++ ppa-assigner/ppa_assigner/tests.py 2013-12-18 18:24:41 +0000
421@@ -28,7 +28,8 @@
422 from django.test import TestCase, TransactionTestCase, skipUnlessDBFeature
423
424 from ci_utils.tastypie.test import TastypieTestCase
425-from ppa_assigner.models import PPA, LaunchpadError
426+from ppa_assigner.models import PPA
427+from ppa_assigner.management.commands.clean_ppas import Command
428
429
430 class TestModel(TestCase):
431@@ -36,53 +37,22 @@
432 def setUp(self):
433 super(TestModel, self).setUp()
434 for n in range(5):
435- PPA.objects.create(name='ci_pool-%s' % str(n).zfill(3))
436-
437- @mock.patch('ppa_assigner.models.PPA.clean_artifacts')
438- def testReserveNoClean(self, clean):
439- p = PPA.reserve('testlockname')
440- self.assertIsNotNone(p)
441- self.assertFalse(clean.called)
442- self.assertEqual('testlockname', p.lockname)
443-
444- reserved = PPA.objects.filter(reserved=True).count()
445- self.assertEqual(1, reserved)
446+ name = 'ci_pool-%s' % str(n).zfill(3)
447+ PPA.objects.create(name=name, ticket_id=0)
448
449 def testReserveTimestamp(self):
450- p = PPA.reserve()
451- delta = datetime.datetime.now() - p.timestamp
452+ ticket = 1234
453+ p = PPA.reserve(ticket)
454+ delta = datetime.datetime.now(p.timestamp.tzinfo) - p.timestamp
455 # the lock should have happened in less than a second
456+ self.assertEqual(PPA.RESERVED, p.state)
457+ self.assertEqual(ticket, p.ticket_id)
458 self.assertLess(delta.seconds, 1)
459
460- @mock.patch('ppa_assigner.models.PPA.clean_artifacts')
461- def testReserveClean(self, clean):
462- p = PPA.reserve('testlockname', True)
463- self.assertIsNotNone(p)
464- self.assertTrue(clean.called)
465- self.assertEqual('testlockname', p.lockname)
466-
467- reserved = PPA.objects.filter(reserved=True).count()
468- self.assertEqual(1, reserved)
469-
470- @mock.patch('ppa_assigner.models.PPA.clean_artifacts')
471- def testReserveCleanFail(self, clean):
472- '''ensure ppa isn't locked if clean operation fails.'''
473- clean.side_effect = RuntimeError
474- locked_before = PPA.objects.filter(reserved=True).count()
475- with self.assertRaises(LaunchpadError):
476- PPA.reserve(clean=True)
477-
478- locked_after = PPA.objects.filter(reserved=True).count()
479- self.assertEqual(locked_before, locked_after)
480-
481 def testReserveNoFree(self):
482- PPA.objects.update(reserved=True)
483+ PPA.objects.update(state=PPA.RESERVED)
484 with self.assertRaises(PPA.DoesNotExist):
485- PPA.reserve()
486-
487- def testCleanOperation(self):
488- # TODO more logic for this
489- PPA.reserve(clean=True)
490+ PPA.reserve(1234)
491
492
493 class TestConcurrency(TransactionTestCase):
494@@ -93,7 +63,8 @@
495 super(TestConcurrency, self).setUp()
496 transaction.enter_transaction_management()
497 for n in range(5):
498- PPA.objects.create(name='ci_pool-%s' % str(n).zfill(3))
499+ name = 'ci_pool-%s' % str(n).zfill(3)
500+ PPA.objects.create(name=name, ticket_id=0)
501 transaction.commit()
502 self.addCleanup(transaction.abort)
503
504@@ -119,7 +90,7 @@
505 def _reserve(self, params):
506 params['signal'].set()
507 start = time.time()
508- PPA.reserve()
509+ PPA.reserve(1234)
510 params['time'] = time.time() - start
511 # connections are per-thread, so close this
512 connection.close()
513@@ -144,49 +115,47 @@
514 def setUp(self):
515 super(TestApi, self).setUp('/api/v1')
516 for n in range(5):
517- PPA.objects.create(name='ci_pool-%s' % str(n).zfill(3))
518+ name = 'ci_pool-%s' % str(n).zfill(3)
519+ PPA.objects.create(name=name, ticket_id=0)
520
521 def testGetAll(self):
522 obj = self.get('ppa/')
523 self.assertEqual(PPA.objects.all().count(), len(obj['objects']))
524
525 def testGetQuery(self):
526- p = PPA.reserve()
527- obj = self.get('ppa/', {'reserved': False})
528- count = PPA.objects.filter(reserved=False).count()
529+ p = PPA.reserve(1234)
530+ obj = self.get('ppa/', {'state': PPA.AVAILABLE})
531+ count = PPA.objects.filter(state=PPA.AVAILABLE).count()
532 self.assertEqual(count, len(obj['objects']))
533
534- obj = self.getResource('ppa/', {'reserved': True})
535+ obj = self.getResource('ppa/', {'state': PPA.RESERVED})
536 self.assertEqual(p.name, obj['name'])
537-
538- p.lockname = 'foo'
539- p.save()
540- obj = self.getResource('ppa/', {'reserved': True})
541- self.assertEqual(p.lockname, obj['lockname'])
542-
543- def testReserveNoClean(self):
544- params = {'lockname': 'foo'}
545+ self.assertEqual(p.ticket_id, obj['ticket_id'])
546+
547+ def testReserve(self):
548+ params = {'ticket_id': 1234}
549 obj = self.createResource('ppa/', params)
550- self.assertEqual(obj['lockname'], params['lockname'])
551- self.assertTrue(obj['reserved'])
552-
553- @mock.patch('ppa_assigner.models.PPA.clean_artifacts')
554- def testReserveClean(self, clean):
555- obj = self.createResource('ppa/', {'lockname': 'foo', 'clean': True})
556- self.assertTrue(obj['reserved'])
557- self.assertTrue(clean.called)
558+ self.assertEqual(obj['ticket_id'], params['ticket_id'])
559
560 def testReserveNoFree(self):
561- PPA.objects.update(reserved=True)
562- params = {'lockname': 'foo'}
563+ PPA.objects.update(state=PPA.DIRTY)
564+ params = {'ticket_id': 1234}
565 with self.assertRaises(PPA.DoesNotExist):
566 self.post('ppa/', params)
567
568- def testFree(self):
569- p = PPA.reserve()
570- self.patch('ppa/%d/' % p.id, {'reserved': False})
571- p = PPA.objects.get(pk=p.id)
572- self.assertFalse(p.reserved)
573+ def testFreeNumeric(self):
574+ '''check we can free a ppa with the numeric value in the db'''
575+ p = PPA.reserve(1234)
576+ self.patch('ppa/%d/' % p.id, {'state': PPA.DIRTY})
577+ p = PPA.objects.get(pk=p.id)
578+ self.assertEqual(p.state, PPA.DIRTY)
579+
580+ def testFreeString(self):
581+ '''check we can free a ppa with a string value'''
582+ p = PPA.reserve(1234)
583+ self.patch('ppa/%d/' % p.id, {'state': 'dirty'})
584+ p = PPA.objects.get(pk=p.id)
585+ self.assertEqual(p.state, PPA.DIRTY)
586
587 @mock.patch('urllib2.urlopen')
588 def testPopulateFail(self, urlopen):
589@@ -219,3 +188,79 @@
590 expected = ['ppa:%s/%s' % (user, x['name']) for x in data['entries']]
591 found = [x.name for x in PPA.objects.all().order_by('name')]
592 self.assertListEqual(expected, found)
593+
594+
595+class TestCleaner(TestCase):
596+
597+ def setUp(self):
598+ super(TestCleaner, self).setUp()
599+ for n in range(5):
600+ name = 'ci_pool-%s' % str(n).zfill(3)
601+ PPA.objects.create(name=name, ticket_id=0)
602+
603+ @mock.patch('ppa_assigner.launchpad._lp_post')
604+ @mock.patch('ppa_assigner.launchpad.get_publishing_history')
605+ def testDirtyToClean(self, get_publishing_history, lp_post):
606+ p = PPA.objects.get(name='ci_pool-002')
607+ p.state = PPA.DIRTY
608+ p.save()
609+
610+ # verify we make one _lp_post call
611+ get_publishing_history.return_value = [
612+ {'self_link': 'foo', 'date_removed': None},
613+ {'self_link': 'foo', 'date_removed': '123'},
614+ ]
615+ resp_200 = mock.Mock()
616+ resp_200.getcode.return_value = 200
617+ lp_post.return_value = resp_200
618+ Command().dirty_to_cleaning()
619+ p = PPA.objects.get(name='ci_pool-002')
620+ self.assertEqual(PPA.CLEANING, p.state)
621+ lp_post.assert_called_once()
622+
623+ @mock.patch('ppa_assigner.launchpad.request_ppa_clean')
624+ def testDirtyToCleanFailure(self, request_clean):
625+ '''make sure state only changes if the call passes.'''
626+ p = PPA.objects.get(name='ci_pool-002')
627+ p.state = PPA.DIRTY
628+ p.save()
629+ request_clean.side_effect = RuntimeError('foo bar mocked error')
630+
631+ Command().dirty_to_cleaning()
632+ p = PPA.objects.get(name='ci_pool-002')
633+ self.assertEqual(PPA.DIRTY, p.state)
634+
635+ @mock.patch('ppa_assigner.launchpad.get_publishing_history')
636+ def testCleaning(self, get_publishing_history):
637+ p = PPA.objects.get(name='ci_pool-002')
638+ p.state = PPA.CLEANING
639+ p.save()
640+
641+ # first pass we still have an item, so the state shouldn't change
642+ get_publishing_history.return_value = [
643+ {'date_removed': None},
644+ {'date_removed': '123'},
645+ ]
646+ Command().cleaning_to_available()
647+ p = PPA.objects.get(name='ci_pool-002')
648+ self.assertEqual(PPA.CLEANING, p.state)
649+
650+ get_publishing_history.return_value = [
651+ {'date_removed': '123'},
652+ {'date_removed': '123'},
653+ ]
654+ Command().cleaning_to_available()
655+ p = PPA.objects.get(name='ci_pool-002')
656+ self.assertEqual(PPA.AVAILABLE, p.state)
657+
658+ @mock.patch('ppa_assigner.launchpad.get_publishing_history')
659+ def testCleaningFailure(self, get_publishing_history):
660+ '''make sure it handles an exception from launchpad gracefully.'''
661+ p = PPA.objects.get(name='ci_pool-002')
662+ p.state = PPA.CLEANING
663+ p.save()
664+ get_publishing_history.side_effect = RuntimeError('foo bar mocked')
665+
666+ Command().cleaning_to_available()
667+ p = PPA.objects.get(name='ci_pool-002')
668+ self.assertEqual(PPA.CLEANING, p.state)
669
670=== modified file 'ppa-assigner/setup.py'
671--- ppa-assigner/setup.py 2013-11-24 18:27:59 +0000
672+++ ppa-assigner/setup.py 2013-12-18 18:24:41 +0000
673@@ -37,6 +37,7 @@
674 'django-tastypie==0.9.15',
675 'South==0.7.5',
676 'mock==1.0.1',
677+ 'oauth==1.0.1'
678 ]
679
680 setup(

Subscribers

People subscribed via source and target branches