Merge lp:~dooferlad/linaro-ci-dashboard/get-command into lp:linaro-ci-dashboard

Proposed by James Tunnicliffe
Status: Merged
Merged at revision: 70
Proposed branch: lp:~dooferlad/linaro-ci-dashboard/get-command
Merge into: lp:linaro-ci-dashboard
Diff against target: 902 lines (+605/-23)
25 files modified
HACKING (+12/-0)
Makefile (+2/-0)
README (+2/-0)
config_template/build_scripts/CIJob.sh (+2/-0)
config_template/config.xml (+2/-2)
config_template/jenkins-jobs/CIJob.xml (+27/-0)
dashboard/frontend/api/api.py (+110/-2)
dashboard/frontend/api/urls.py (+2/-0)
dashboard/frontend/ci_job/migrations/0001_initial.py (+60/-0)
dashboard/frontend/ci_job/models/__init__.py (+1/-0)
dashboard/frontend/ci_job/models/ci_job.py (+135/-0)
dashboard/frontend/ci_job/urls.py (+31/-0)
dashboard/frontend/ci_job/views/ci_job_build_view.py (+25/-0)
dashboard/frontend/ci_job/views/ci_job_detail_view.py (+24/-0)
dashboard/frontend/management/commands/jenkins.py (+3/-3)
dashboard/frontend/management/commands/tick.py (+36/-0)
dashboard/frontend/models/loop_reference.py (+1/-0)
dashboard/frontend/tests/test_api.py (+108/-13)
dashboard/frontend/views/index_view.py (+2/-0)
dashboard/jenkinsserver/models/jenkins_server.py (+12/-1)
dashboard/lib/jenkins_dashboard.py (+4/-1)
dashboard/lib/xml_to_dict.py (+1/-1)
dashboard/settings.py (+1/-0)
dashboard/urls.py (+1/-0)
requirements.txt (+1/-0)
To merge this branch: bzr merge lp:~dooferlad/linaro-ci-dashboard/get-command
Reviewer Review Type Date Requested Status
Milo Casagrande (community) Approve
Review via email: mp+150548@code.launchpad.net

Description of the change

API changes + tests to support the CLI get command.

Also changed the Jenkins update behaviour so the user is told about a Jenkins update rather than required to perform the update. This is partially because updates can be slow and inconvenient (especially during a demo), but also because sometimes the Jenkins API changes and it can break how the dashboard interacts with it. A better fix would be to have a tested Jenkins version listed in the branch which is the one downloaded and checked against. That version can be bumped as a separate downlaod + test procedure.

To post a comment you must log in.
Revision history for this message
Milo Casagrande (milo) wrote :

Hello James,

looks good to me. It took me a while to go through, also for fancy tastypie terminology for serializing objects.

Randomly, I have one error running the tests, but have no idea if it is related to Jenkins or something else. Last time I ran the tests I didn't have any.
Speaking of Jenkins, we should try to stick with what we will probably be using in ci.l.o, I guess we will be based on that.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'HACKING'
2--- HACKING 2012-09-21 09:24:01 +0000
3+++ HACKING 2013-02-26 12:48:40 +0000
4@@ -49,6 +49,18 @@
5
6 Tests should be written and defined inside each sub-application.
7
8+When defining a new application, make sure the model app_label matches what is
9+listed in settings.py:
10+
11+class CIJob(Loop):
12+ """Defines the base class for a CI Job."""
13+ class Meta:
14+ app_label = 'ci_job'
15+
16+INSTALLED_APPS = (
17+...
18+ 'frontend.ci_job',
19+
20 == Class Hierarchy ==
21
22 === Templates & Views ===
23
24=== modified file 'Makefile'
25--- Makefile 2012-09-24 13:18:02 +0000
26+++ Makefile 2013-02-26 12:48:40 +0000
27@@ -24,6 +24,8 @@
28 echo "No changes to android_textfield_loop models."
29 python dashboard/manage.py schemamigration hwpack_loop --auto || \
30 echo "No changes to hwpack_loop models."
31+ python dashboard/manage.py schemamigration ci_job --auto || \
32+ echo "No changes to ci_job models."
33 python dashboard/manage.py migrate
34
35 migrate: syncdb
36
37=== modified file 'README'
38--- README 2013-02-01 11:49:24 +0000
39+++ README 2013-02-26 12:48:40 +0000
40@@ -17,6 +17,8 @@
41 build-essential
42 openjdk-6-jre-headless
43 python-tastypie
44+ python-simplejson
45+ python-requests
46
47 To unit test the API that tastypie provides you need Django version of at least
48 1.4. If you have Django 1.3 the API will still work, but not be unit tested.
49
50=== added file 'config_template/build_scripts/CIJob.sh'
51--- config_template/build_scripts/CIJob.sh 1970-01-01 00:00:00 +0000
52+++ config_template/build_scripts/CIJob.sh 2013-02-26 12:48:40 +0000
53@@ -0,0 +1,2 @@
54+touch done
55+echo "done" > MANIFEST
56
57=== modified file 'config_template/config.xml'
58--- config_template/config.xml 2012-09-20 08:46:37 +0000
59+++ config_template/config.xml 2013-02-26 12:48:40 +0000
60@@ -50,7 +50,7 @@
61 </views>
62 <primaryView>All</primaryView>
63 <slaveAgentPort>0</slaveAgentPort>
64- <label>IntegrationLoop AndroidLoop KernelLoop AndroidTextFieldLoop HwpackLoop</label>
65+ <label>IntegrationLoop AndroidLoop KernelLoop AndroidTextFieldLoop HwpackLoop CIJob</label>
66 <nodeProperties/>
67 <globalNodeProperties/>
68-</hudson>
69\ No newline at end of file
70+</hudson>
71
72=== added file 'config_template/jenkins-jobs/CIJob.xml'
73--- config_template/jenkins-jobs/CIJob.xml 1970-01-01 00:00:00 +0000
74+++ config_template/jenkins-jobs/CIJob.xml 2013-02-26 12:48:40 +0000
75@@ -0,0 +1,27 @@
76+<?xml version='1.0' encoding='UTF-8'?>
77+<project>
78+ <actions/>
79+ <description></description>
80+ <keepDependencies>false</keepDependencies>
81+ <properties/>
82+ <scm class="hudson.scm.NullSCM"/>
83+ <assignedNode>{assigned_node}</assignedNode>
84+ <canRoam>false</canRoam>
85+ <disabled>false</disabled>
86+ <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
87+ <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
88+ <triggers class="vector"/>
89+ <concurrentBuild>false</concurrentBuild>
90+ <builders>
91+ <hudson.tasks.Shell>
92+ <command>{build_script}</command>
93+ </hudson.tasks.Shell>
94+ </builders>
95+ <publishers>
96+ <hudson.tasks.ArtifactArchiver>
97+ <artifacts>*</artifacts>
98+ <latestOnly>false</latestOnly>
99+ </hudson.tasks.ArtifactArchiver>
100+ </publishers>
101+ <buildWrappers/>
102+</project>
103
104=== modified file 'dashboard/frontend/api/api.py'
105--- dashboard/frontend/api/api.py 2013-02-04 10:59:49 +0000
106+++ dashboard/frontend/api/api.py 2013-02-26 12:48:40 +0000
107@@ -16,9 +16,13 @@
108 # along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
109
110 from tastypie.resources import ModelResource
111-from tastypie.authorization import DjangoAuthorization
112-from tastypie.authentication import ApiKeyAuthentication
113+from tastypie.authorization import DjangoAuthorization, Authorization
114+from tastypie.authentication import ApiKeyAuthentication, Authentication
115 from dashboard.frontend.models.loop import Loop
116+from frontend.ci_job.models.ci_job import CIJob
117+from frontend.models.loop_build import LoopBuild
118+from tastypie.constants import ALL, ALL_WITH_RELATIONS
119+from tastypie import fields
120
121 """
122 Add API endpoint logic here. The documentation to do this can be found here:
123@@ -54,3 +58,107 @@
124 resource_name = 'loops'
125 authorization = DjangoAuthorization()
126 authentication = ApiKeyAuthentication()
127+
128+ filtering = {'name': ALL_WITH_RELATIONS}
129+
130+
131+class CIJobResource(ModelResource):
132+ class Meta:
133+ """Returns a list of all loops."""
134+ queryset = CIJob.objects.all()
135+ resource_name = 'jobs'
136+ list_allowed_methods = ['get', 'post']
137+ detail_allowed_methods = ['get', 'post', 'put', 'delete']
138+
139+ #http://django-tastypie.readthedocs.org/en/latest/authorization.html
140+ # Currently everyone who has an API key is equal. Suggest we tie this
141+ # down using a custom Authorization class (see above link) so anyone
142+ # can create and modify their own jobs, but only some users have
143+ # the ability to delete and modify other users' jobs.
144+ authorization = Authorization() #DjangoAuthorization()
145+ authentication = ApiKeyAuthentication()
146+
147+
148+class CIRunResource(ModelResource):
149+ loop = fields.ForeignKey(LoopsResource, 'loop')
150+
151+ class Meta:
152+ """Returns a list of all loops."""
153+ queryset = LoopBuild.objects.all()
154+ resource_name = 'runs'
155+ list_allowed_methods = ['get', 'post']
156+ detail_allowed_methods = ['get', 'post', 'put', 'delete']
157+ filtering = {'name': 'exact',
158+ 'build_number': 'exact',
159+ 'loop': ALL_WITH_RELATIONS}
160+
161+ #http://django-tastypie.readthedocs.org/en/latest/authorization.html
162+ # Currently everyone who has an API key is equal. Suggest we tie this
163+ # down using a custom Authorization class (see above link) so anyone
164+ # can create and modify their own jobs, but only some users have
165+ # the ability to delete and modify other users' jobs.
166+ authorization = Authorization() #DjangoAuthorization()
167+ authentication = ApiKeyAuthentication()
168+
169+ def dehydrate(self, bundle):
170+ data = {}
171+ # We entirely rewrite this...
172+ data["status"] = bundle.data["status"]
173+ data["get_build_result"] = bundle.obj.get_build_result()
174+ data["build_finished"] = bundle.obj.build_finished()
175+ data["list_artifacts"] = bundle.obj.list_artifacts()
176+ data["build_number"] = bundle.data["build_number"]
177+ data["name"] = bundle.obj.loop.name
178+ #data["get_artifact_url"] = bundle.obj.get_artifact_url()
179+ #data["get_artifact_content"] = bundle.obj.get_artifact_content()
180+ bundle.data = data
181+ return bundle
182+
183+ def full_hydrate(self, bundle):
184+ """
185+ Given a populated bundle, distill it and turn it back into
186+ a full-fledged object instance.
187+
188+ This is a copy of the tastypie full_hydrate code with a single
189+ modification to not try to fill in LoopBuild.loop from the API supplied
190+ data.
191+
192+ Note that this won't work because we won't create a full object:
193+ loop_object = self.fields["loop"]
194+ del(self.fields["loop"])
195+ super(self.__class__, self).full_hydrate(bundle)
196+ self.fields["loop"] = loop_object
197+ """
198+ if bundle.obj is None:
199+ bundle.obj = self._meta.object_class()
200+
201+ for field_name, field_object in self.fields.items():
202+ # Fill in all attributes apart from loop, which is created during
203+ # the (later) hydrate phase.
204+ if field_object.attribute and field_object.attribute != "loop":
205+ value = field_object.hydrate(bundle)
206+
207+ if value is not None or field_object.null:
208+ # We need to avoid populating M2M data here as that will
209+ # cause things to blow up.
210+ if not getattr(field_object, 'is_related', False):
211+ setattr(bundle.obj, field_object.attribute, value)
212+ elif not getattr(field_object, 'is_m2m', False):
213+ if value is not None:
214+ setattr(bundle.obj, field_object.attribute,
215+ value.obj)
216+
217+ # Check for an optional method to do further hydration.
218+ method = getattr(self, "hydrate_%s" % field_name, None)
219+
220+ if method:
221+ bundle = method(bundle)
222+
223+ bundle = self.hydrate(bundle)
224+ return bundle
225+
226+ def hydrate(self, bundle):
227+ bundle.obj.loop = Loop.objects.get(name=bundle.data["name"])
228+ bundle.obj.status = 'unscheduled'
229+ bundle.obj.duration = 0.0
230+ return bundle
231
232=== modified file 'dashboard/frontend/api/urls.py'
233--- dashboard/frontend/api/urls.py 2013-02-04 17:55:49 +0000
234+++ dashboard/frontend/api/urls.py 2013-02-26 12:48:40 +0000
235@@ -7,6 +7,8 @@
236 # Register new resources here (probably defined in api.py)
237 v1_api.register(LoginTestResource())
238 v1_api.register(LoopsResource())
239+v1_api.register(CIJobResource())
240+v1_api.register(CIRunResource())
241
242 urlpatterns = patterns('',
243 (r'^api/', include(v1_api.urls)),
244
245=== added directory 'dashboard/frontend/ci_job'
246=== added file 'dashboard/frontend/ci_job/__init__.py'
247=== added directory 'dashboard/frontend/ci_job/migrations'
248=== added file 'dashboard/frontend/ci_job/migrations/0001_initial.py'
249--- dashboard/frontend/ci_job/migrations/0001_initial.py 1970-01-01 00:00:00 +0000
250+++ dashboard/frontend/ci_job/migrations/0001_initial.py 2013-02-26 12:48:40 +0000
251@@ -0,0 +1,60 @@
252+# -*- coding: utf-8 -*-
253+import datetime
254+from south.db import db
255+from south.v2 import SchemaMigration
256+from django.db import models
257+
258+
259+class Migration(SchemaMigration):
260+
261+ def forwards(self, orm):
262+ # Adding model 'CIJob'
263+ db.create_table('ci_job_cijob', (
264+ ('loop_ptr', self.gf('django.db.models.fields.related.OneToOneField')(to=orm['frontend.Loop'], unique=True, primary_key=True)),
265+ ('vcs_url', self.gf('django.db.models.fields.CharField')(max_length=255)),
266+ ('vcs_branch', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
267+ ('vcs_tag', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
268+ ('file_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
269+ ('class_name', self.gf('django.db.models.fields.CharField')(max_length=255)),
270+ ('run_when_ready', self.gf('django.db.models.fields.BooleanField')(default=False)),
271+ ))
272+ db.send_create_signal('ci_job', ['CIJob'])
273+
274+
275+ def backwards(self, orm):
276+ # Deleting model 'CIJob'
277+ db.delete_table('ci_job_cijob')
278+
279+
280+ models = {
281+ 'ci_job.cijob': {
282+ 'Meta': {'object_name': 'CIJob', '_ormbases': ['frontend.Loop']},
283+ 'class_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
284+ 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
285+ 'loop_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['frontend.Loop']", 'unique': 'True', 'primary_key': 'True'}),
286+ 'run_when_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
287+ 'vcs_branch': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
288+ 'vcs_tag': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
289+ 'vcs_url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
290+ },
291+ 'frontend.loop': {
292+ 'Meta': {'object_name': 'Loop'},
293+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
294+ 'is_official': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
295+ 'is_restricted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
296+ 'lava_tests': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
297+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'}),
298+ 'next_loop': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'previous_loop'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['frontend.Loop']", 'blank': 'True', 'unique': 'True'}),
299+ 'server': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['jenkinsserver.JenkinsServer']", 'null': 'True', 'on_delete': 'models.SET_NULL'}),
300+ 'type': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'})
301+ },
302+ 'jenkinsserver.jenkinsserver': {
303+ 'Meta': {'object_name': 'JenkinsServer'},
304+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
305+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
306+ 'url': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
307+ 'username': ('django.db.models.fields.CharField', [], {'max_length': '100'})
308+ }
309+ }
310+
311+ complete_apps = ['ci_job']
312\ No newline at end of file
313
314=== added file 'dashboard/frontend/ci_job/migrations/__init__.py'
315=== added directory 'dashboard/frontend/ci_job/models'
316=== added file 'dashboard/frontend/ci_job/models/__init__.py'
317--- dashboard/frontend/ci_job/models/__init__.py 1970-01-01 00:00:00 +0000
318+++ dashboard/frontend/ci_job/models/__init__.py 2013-02-26 12:48:40 +0000
319@@ -0,0 +1,1 @@
320+from frontend.ci_job.models.ci_job import *
321
322=== added file 'dashboard/frontend/ci_job/models/ci_job.py'
323--- dashboard/frontend/ci_job/models/ci_job.py 1970-01-01 00:00:00 +0000
324+++ dashboard/frontend/ci_job/models/ci_job.py 2013-02-26 12:48:40 +0000
325@@ -0,0 +1,135 @@
326+# Copyright (C) 2012 Linaro
327+#
328+# This file is part of linaro-ci-dashboard.
329+#
330+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
331+# it under the terms of the GNU Affero General Public License as published by
332+# the Free Software Foundation, either version 3 of the License, or
333+# (at your option) any later version.
334+#
335+# linaro-ci-dashboard is distributed in the hope that it will be useful,
336+# but WITHOUT ANY WARRANTY; without even the implied warranty of
337+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
338+# GNU Affero General Public License for more details.
339+#
340+# You should have received a copy of the GNU Affero General Public License
341+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
342+
343+from django.db import models
344+from frontend.models.loop import Loop
345+from jenkinsserver.models.jenkins_server import JenkinsServer
346+from frontend.models.loop_build import LoopBuild
347+#from django.contrib.auth.models import User
348+
349+
350+class CIJob(Loop):
351+ """Defines the base class for a CI Job."""
352+ class Meta:
353+ app_label = 'ci_job'
354+
355+ vcs_url = models.CharField(max_length=255,
356+ verbose_name='Configuration repository')
357+ vcs_branch = models.CharField(max_length=255,
358+ verbose_name='Configuration branch',
359+ default='')
360+ vcs_tag = models.CharField(max_length=255,
361+ verbose_name='Configuration tag',
362+ default='')
363+ file_name = models.CharField(max_length=255,
364+ verbose_name='Configuration file name')
365+ class_name = models.CharField(max_length=255,
366+ verbose_name='Configuration class name')
367+ run_when_ready = models.BooleanField(default=False)
368+
369+ #job_owner = models.ForeignKey(User, editable=False,
370+ # verbose_name='Job Owner')
371+
372+ def save(self, *args, **kwargs):
373+ self.server = JenkinsServer.objects.get(id=1)
374+ self.type = self.__class__.__name__
375+ super(self.__class__, self).save(*args, **kwargs)
376+ # We do not want a failure in remote server to affect the
377+ # CI Loop flow.
378+
379+ self.server.create_or_update_loop(self)
380+
381+
382+ def schedule_build(self, parameters=None):
383+ parameters = {'parameter': [{'delay': '0'}]}
384+ return super(self.__class__, self).schedule_build(parameters)
385+
386+ def base64_config(self,
387+ include=None,
388+ exclude=None,
389+ capitalize=False,
390+ upper=False):
391+ exclude = ['id', 'server', 'loop_ptr', 'next_loop', 'name', 'type']
392+ return super(CIJob, self).base64_config(include=include,
393+ exclude=exclude,
394+ capitalize=capitalize,
395+ upper=True)
396+
397+ def preconfigure(self, configuration=None):
398+ """Makes a build parameter dict from build result configuration.
399+
400+ This method should be overridden by subclasses.
401+ """
402+ if isinstance(configuration, dict):
403+ # The name is the name of the loop that is calling the chained one,
404+ # not the one we want to actually configure, we drop it.
405+ configuration.pop('name')
406+ unmatched_values = u''
407+ for key, value in configuration.iteritems():
408+ if hasattr(self, key.lower()):
409+ setattr(self, key.lower(), value)
410+ else:
411+ # If the attr cannot be found, we throw everything in
412+ # user_defined_values since the text field is free form.
413+ unmatched_values = u'%s=%s\n' % (key.upper(), str(value))
414+
415+ self.user_defined_values = unmatched_values
416+ self.save()
417+ else:
418+ self.log.error("Parameter was not a 'dict' instance.")
419+ return {}
420+
421+ @models.permalink
422+ def get_detail_url(self):
423+ return 'CIJobDetail', (), {'slug': self.name}
424+
425+ @models.permalink
426+ def get_build_url(self):
427+ return 'CIJobBuild', (), {'slug': self.name}
428+
429+ @models.permalink
430+ def get_update_url(self):
431+ return 'CIJobUpdate', (), {'slug': self.name}
432+
433+ @classmethod
434+ def schedule_unscheduled_jobs(self):
435+ unscheduled_builds = LoopBuild.objects.filter(status='unscheduled')
436+ for build in unscheduled_builds:
437+ build.status = 'scheduled'
438+
439+ build.remote_number = build.loop.server.schedule_job_build(
440+ build.loop.name)
441+ build.save()
442+
443+ def process_build_results(self, loop_build):
444+ """This method gets called with list of artifact files from a build.
445+ Its responsibility is to categorize artifacts, and prepare result data
446+ to be passed to downstream loop in chain. If needed, this method can
447+ access content of artifacts (not just list of them).
448+ """
449+
450+ all_files_zip = "*zip*/archive.zip"
451+ urls = {
452+ all_files_zip:
453+ loop_build.get_artifact_url(all_files_zip),
454+ }
455+
456+ files = loop_build.list_artifacts()
457+ for filename in files:
458+ urls[filename] = loop_build.get_artifact_url(filename)
459+
460+ return urls
461
462=== added file 'dashboard/frontend/ci_job/urls.py'
463--- dashboard/frontend/ci_job/urls.py 1970-01-01 00:00:00 +0000
464+++ dashboard/frontend/ci_job/urls.py 2013-02-26 12:48:40 +0000
465@@ -0,0 +1,31 @@
466+# Copyright (C) 2012 Linaro
467+#
468+# This file is part of linaro-ci-dashboard.
469+#
470+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
471+# it under the terms of the GNU Affero General Public License as published by
472+# the Free Software Foundation, either version 3 of the License, or
473+# (at your option) any later version.
474+#
475+# linaro-ci-dashboard is distributed in the hope that it will be useful,
476+# but WITHOUT ANY WARRANTY; without even the implied warranty of
477+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
478+# GNU Affero General Public License for more details.
479+#
480+# You should have received a copy of the GNU Affero General Public License
481+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
482+
483+from django.conf.urls.defaults import patterns, url
484+
485+from frontend.ci_job.views.ci_job_detail_view \
486+ import CIJobDetailView
487+from frontend.ci_job.views.ci_job_build_view \
488+ import CIJobBuildView
489+
490+
491+urlpatterns = patterns('',
492+ url(r'^ci_job/detail/(?P<slug>[\w|\W]+)/$',
493+ CIJobDetailView.as_view(), name='CIJobDetail'),
494+ url(r'^ci_job/build/(?P<slug>[\w|\W]+)/$',
495+ CIJobBuildView.as_view(), name='CIJobBuild'),
496+)
497
498=== added directory 'dashboard/frontend/ci_job/views'
499=== added file 'dashboard/frontend/ci_job/views/__init__.py'
500=== added file 'dashboard/frontend/ci_job/views/ci_job_build_view.py'
501--- dashboard/frontend/ci_job/views/ci_job_build_view.py 1970-01-01 00:00:00 +0000
502+++ dashboard/frontend/ci_job/views/ci_job_build_view.py 2013-02-26 12:48:40 +0000
503@@ -0,0 +1,25 @@
504+#!/usr/bin/env python
505+# Copyright (C) 2012 Linaro
506+#
507+# This file is part of linaro-ci-dashboard.
508+#
509+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
510+# it under the terms of the GNU Affero General Public License as published by
511+# the Free Software Foundation, either version 3 of the License, or
512+# (at your option) any later version.
513+#
514+# linaro-ci-dashboard is distributed in the hope that it will be useful,
515+# but WITHOUT ANY WARRANTY; without even the implied warranty of
516+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
517+# GNU Affero General Public License for more details.
518+#
519+# You should have received a copy of the GNU Affero General Public License
520+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
521+
522+from frontend.ci_job.models.ci_job import CIJob
523+from frontend.views.loop_build_view import LoopBuildView
524+
525+
526+class CIJobBuildView(LoopBuildView):
527+
528+ model = CIJob
529
530=== added file 'dashboard/frontend/ci_job/views/ci_job_detail_view.py'
531--- dashboard/frontend/ci_job/views/ci_job_detail_view.py 1970-01-01 00:00:00 +0000
532+++ dashboard/frontend/ci_job/views/ci_job_detail_view.py 2013-02-26 12:48:40 +0000
533@@ -0,0 +1,24 @@
534+# Copyright (C) 2012 Linaro
535+#
536+# This file is part of linaro-ci-dashboard.
537+#
538+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
539+# it under the terms of the GNU Affero General Public License as published by
540+# the Free Software Foundation, either version 3 of the License, or
541+# (at your option) any later version.
542+#
543+# linaro-ci-dashboard is distributed in the hope that it will be useful,
544+# but WITHOUT ANY WARRANTY; without even the implied warranty of
545+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
546+# GNU Affero General Public License for more details.
547+#
548+# You should have received a copy of the GNU Affero General Public License
549+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
550+
551+from frontend.ci_job.models.ci_job import CIJob
552+from frontend.views.loop_detail_view import LoopDetailView
553+
554+
555+class CIJobDetailView(LoopDetailView):
556+
557+ model = CIJob
558
559=== modified file 'dashboard/frontend/management/commands/jenkins.py'
560--- dashboard/frontend/management/commands/jenkins.py 2012-09-18 13:06:35 +0000
561+++ dashboard/frontend/management/commands/jenkins.py 2013-02-26 12:48:40 +0000
562@@ -144,9 +144,9 @@
563 downloaded_size = int(os.stat(filename).st_size)
564 response.close()
565 if actual_size != downloaded_size:
566- raise CommandError("Size mismatch between remote and local "\
567- "jenkins.war, remove %s to upgrade to new "\
568- "version" % filename)
569+ print "Size mismatch between remote and local "\
570+ "jenkins.war, remove %s to upgrade to new "\
571+ "version" % filename
572
573 return 0
574
575
576=== added file 'dashboard/frontend/management/commands/tick.py'
577--- dashboard/frontend/management/commands/tick.py 1970-01-01 00:00:00 +0000
578+++ dashboard/frontend/management/commands/tick.py 2013-02-26 12:48:40 +0000
579@@ -0,0 +1,36 @@
580+# Copyright (C) 2013 Linaro
581+#
582+# This file is part of linaro-ci-dashboard.
583+#
584+# linaro-ci-dashboard is free software: you can redistribute it and/or modify
585+# it under the terms of the GNU Affero General Public License as published by
586+# the Free Software Foundation, either version 3 of the License, or
587+# (at your option) any later version.
588+#
589+# linaro-ci-dashboard is distributed in the hope that it will be useful,
590+# but WITHOUT ANY WARRANTY; without even the implied warranty of
591+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
592+# GNU Affero General Public License for more details.
593+#
594+# You should have received a copy of the GNU Affero General Public License
595+# along with linaro-ci-dashboard. If not, see <http://www.gnu.org/licenses/>.
596+# Django settings for dashboard project.
597+
598+from django.core.management.base import BaseCommand
599+from frontend.ci_job.models.ci_job import CIJob
600+from jenkinsserver.models.jenkins_server import JenkinsServer
601+from django.conf import settings
602+
603+
604+class Command(BaseCommand):
605+ args = ""
606+ help = "Run background tasks. Needs to be run periodically."
607+
608+ def __init__(self):
609+ super(self.__class__, self).__init__()
610+ self.jenkins_id = settings.JENKINS_DB_ID
611+
612+ def handle(self, *args, **options):
613+ CIJob.schedule_unscheduled_jobs()
614+ server = JenkinsServer.objects.get(id=self.jenkins_id)
615+ server.sync_jobs_and_builds()
616
617=== modified file 'dashboard/frontend/models/loop_reference.py'
618--- dashboard/frontend/models/loop_reference.py 2012-09-18 12:16:59 +0000
619+++ dashboard/frontend/models/loop_reference.py 2013-02-26 12:48:40 +0000
620@@ -23,3 +23,4 @@
621 INTEGRATION_LOOP = ('IntegrationLoop')
622 ANDROID_TEXTFIELD_LOOP = ('AndroidTextFieldLoop')
623 HWPACK_LOOP = ('HwpackLoop')
624+CI_JOB = ('CIJob')
625
626=== modified file 'dashboard/frontend/tests/test_api.py'
627--- dashboard/frontend/tests/test_api.py 2013-02-04 17:55:49 +0000
628+++ dashboard/frontend/tests/test_api.py 2013-02-26 12:48:40 +0000
629@@ -19,9 +19,12 @@
630 from django.test import LiveServerTestCase
631 from tastypie.models import ApiKey
632 from dashboard.frontend.models.loop import Loop
633+from dashboard.frontend.models.loop_build import LoopBuild
634 from dashboard.frontend.android_build.models.android_loop import AndroidLoop
635+from dashboard.frontend.ci_job.models.ci_job import CIJob
636 import requests
637 from urlparse import urljoin
638+import simplejson
639
640
641 class ApiTests(LiveServerTestCase):
642@@ -32,13 +35,18 @@
643 def setUp(self):
644 super(ApiTests, self).setUp()
645
646+ self.settings = {}
647+ self.settings["ci_server"] = "http://localhost:8081"
648+ self.settings["api_prefix"] = "/api/dev/"
649+ self.settings["username"] = 'daniel'
650+ self.settings["password"] = 'pass'
651+ self.settings["api_key"] = "0"
652+
653 # Create a user.
654- self.username = 'daniel'
655- self.password = 'pass'
656 self.user = User.objects.create_user(
657- self.username, 'daniel@example.com', self.password)
658-
659- self.api_key = "0"
660+ self.settings["username"],
661+ 'daniel@example.com',
662+ self.settings["password"])
663
664 # Add a couple of jobs to test against.
665 self.android_loop = AndroidLoop()
666@@ -51,16 +59,35 @@
667 self.loop.type = Loop.__class__.__name__
668 self.loop.save()
669
670- def get(self, url):
671- payload = {"username": self.username,
672- "api_key": self.api_key,
673- "format": "json"}
674- url = urljoin("http://localhost:8081", "/api/dev/" + url)
675- return requests.get(url, params=payload)
676+ def get(self, url, in_params=None):
677+ url, params = self._prep_request(url, in_params)
678+ return requests.get(url, params=params)
679+
680+ def post(self, url, data, in_params=None):
681+ url, params = self._prep_request(url, in_params)
682+ return requests.post(url, data=data, params=params,
683+ headers={"content-type": "application/json"})
684+
685+ def _prep_request(self, url, in_params=None):
686+ params = {"username": self.settings["username"],
687+ "api_key": self.settings["api_key"],
688+ "format": "json"}
689+
690+ url = urljoin(self.settings["ci_server"],
691+ self.settings["api_prefix"] + url)
692+
693+ # If we don't have a trailing slash, we will just be redirected. Avoid
694+ # the extra call.
695+ if url[-1] != "/":
696+ url += "/"
697+
698+ if in_params:
699+ params.update(in_params)
700+
701+ return url, params
702
703 def login(self):
704- self.api_key = ApiKey.objects.create(user=self.user).key
705- #self.create_apikey(self.username, self.api_key)
706+ self.settings["api_key"] = ApiKey.objects.create(user=self.user).key
707
708 def test_login_test_unauthorzied(self):
709 r = self.get("login_test")
710@@ -93,3 +120,71 @@
711 api_test_names = {json[0]['name'], json[1]['name']}
712 test_names = {self.android_loop.name, self.loop.name}
713 self.assertEqual(api_test_names, test_names)
714+
715+ def test_add_job(self):
716+ self.login()
717+ data = {
718+ "vcs_url": "vcsurl",
719+ "file_name": "filename",
720+ "class_name": "classname",
721+ "run": False,
722+ "name": "jobname"
723+ }
724+ json_data = simplejson.dumps(data)
725+
726+ r = self.post("jobs", json_data, {})
727+ self.assertEqual(r.status_code, requests.codes.created)
728+
729+ jobs = CIJob.objects.all()
730+
731+ self.assertEqual(len(jobs), 1)
732+ self.assertEqual(jobs[0].name, data["name"])
733+
734+ def test_run_job(self):
735+ self.login()
736+
737+ # First, create a dummy job
738+ job = CIJob()
739+ job.vcs_url = "vcsurl"
740+ job.name = "jobname"
741+ job.save()
742+
743+ # Now run it
744+ data = {
745+ "name": "jobname"
746+ }
747+ json_data = simplejson.dumps(data)
748+
749+ r = self.post("runs", json_data, {})
750+ self.assertEqual(r.status_code, requests.codes.created)
751+
752+ runs = LoopBuild.objects.all()
753+
754+ self.assertEqual(len(runs), 1)
755+ self.assertEqual(runs[0].loop.name, data["name"])
756+
757+ def test_get_job(self):
758+ self.login()
759+
760+ # Create a dummy job
761+ job = CIJob()
762+ job.vcs_url = "vcsurl"
763+ job.name = "jobname"
764+ job.save()
765+
766+ # Create a dummy run
767+ run = LoopBuild()
768+ run.build_number = 1
769+ run.status = "SUCCESS"
770+ run.duration = 0
771+ run.loop = job
772+ run.save()
773+
774+ r = self.get("runs")
775+ self.assertEqual(r.status_code, requests.codes.ok)
776+
777+ run_data = r.json()["objects"]
778+ self.assertEqual(len(run_data), 1)
779+ self.assertEqual(run_data[0]["build_number"], run.build_number)
780+ self.assertEqual(run_data[0]["status"], run.status)
781+ self.assertEqual(run_data[0]["name"], job.name)
782
783=== modified file 'dashboard/frontend/views/index_view.py'
784--- dashboard/frontend/views/index_view.py 2012-09-13 11:37:13 +0000
785+++ dashboard/frontend/views/index_view.py 2013-02-26 12:48:40 +0000
786@@ -25,6 +25,7 @@
787 from frontend.android_textfield_loop.models.android_textfield_loop \
788 import AndroidTextFieldLoop
789 from frontend.hwpack_loop.models.hwpack_loop import HwpackLoop
790+from frontend.ci_job.models.ci_job import CIJob
791
792 class IndexView(TemplateView):
793
794@@ -42,4 +43,5 @@
795 context['kernel_loops'] = KernelLoop.objects.all()
796 context['android_textfield_loops'] = AndroidTextFieldLoop.objects.all()
797 context['hwpack_loops'] = HwpackLoop.objects.all()
798+ context['ci_jobs'] = CIJob.objects.all()
799 return context
800
801=== modified file 'dashboard/jenkinsserver/models/jenkins_server.py'
802--- dashboard/jenkinsserver/models/jenkins_server.py 2012-10-04 15:26:28 +0000
803+++ dashboard/jenkinsserver/models/jenkins_server.py 2013-02-26 12:48:40 +0000
804@@ -24,6 +24,7 @@
805 from lib.logger import Logger
806 from lib.template import TextTemplate, XMLTemplate
807 from dashboard.lib.xml_to_dict import DictToXml
808+import requests
809
810
811 class JenkinsServer(models.Model):
812@@ -89,6 +90,8 @@
813 # to store as build results.
814 loop = build.loop.get_child_object()
815 values = loop.process_build_results(build)
816+ values["*console_url*"] = self.jenkins.get_build_output_url(
817+ build.loop.name, build.remote_number)
818 build.result_xml = DictToXml(values).dict_to_tree()
819 build.save()
820
821@@ -152,7 +155,15 @@
822
823 def schedule_job_build(self, jobname, parameters=None):
824 """Trigger build job on jenkins. Return the next build number."""
825- self.jenkins.build_job(jobname, parameters)
826+
827+ # Note that python-jenkins uses urllib2 in such a way that new job
828+ # API requests will always be an HTTP GET. In recent versions of
829+ # Jenkins this is invalid. Using requests to work around this, but
830+ # using the python-jenkins library to generate the job URL.
831+ requests.post(self.jenkins.build_job_url(jobname),
832+ auth=(settings.JENKINS_ADMIN_USER,
833+ settings.JENKINS_ADMIN_PASSWD),
834+ params=parameters)
835 job_info = self.jenkins.get_job_info(jobname)
836 return job_info["nextBuildNumber"]
837
838
839=== modified file 'dashboard/lib/jenkins_dashboard.py'
840--- dashboard/lib/jenkins_dashboard.py 2012-09-24 08:21:17 +0000
841+++ dashboard/lib/jenkins_dashboard.py 2013-02-26 12:48:40 +0000
842@@ -43,11 +43,14 @@
843 raise jenkins.JenkinsException(
844 "Could not parse JSON info for job[%s]" % name)
845
846+ def get_build_output_url(self, name, build_number):
847+ return self.server + self.BUILD_CONSOLE % locals()
848+
849 def get_build_output(self, name, build_number):
850
851 try:
852 response = self.jenkins_open(
853- urllib2.Request(self.server + self.BUILD_CONSOLE % locals()))
854+ urllib2.Request(self.get_build_output_url(name, build_number)))
855 if response:
856 return response
857 else:
858
859=== modified file 'dashboard/lib/xml_to_dict.py'
860--- dashboard/lib/xml_to_dict.py 2012-09-07 12:48:04 +0000
861+++ dashboard/lib/xml_to_dict.py 2013-02-26 12:48:40 +0000
862@@ -48,7 +48,7 @@
863 :param xml: The XML tree as a string.
864 :type xml str
865 """
866- if not isinstance(xml, str):
867+ if not (isinstance(xml, str) or isinstance(xml, unicode)):
868 raise XmlToDictException("Type of parameter is not 'str', "
869 "got '%s' instead." % type(xml).__name__)
870 super(XmlToDict, self).__init__()
871
872=== modified file 'dashboard/settings.py'
873--- dashboard/settings.py 2013-01-30 17:20:40 +0000
874+++ dashboard/settings.py 2013-02-26 12:48:40 +0000
875@@ -158,6 +158,7 @@
876 'frontend.android_textfield_loop',
877 'frontend.hwpack_loop',
878 'frontend.api',
879+ 'frontend.ci_job',
880 'tastypie',
881 'south',
882 # Uncomment the next line to enable admin documentation:
883
884=== modified file 'dashboard/urls.py'
885--- dashboard/urls.py 2013-01-21 16:16:03 +0000
886+++ dashboard/urls.py 2013-02-26 12:48:40 +0000
887@@ -40,4 +40,5 @@
888 url(r'^', include('dashboard.frontend.android_textfield_loop.urls')),
889 url(r'^', include('dashboard.frontend.hwpack_loop.urls')),
890 url(r'^', include('dashboard.frontend.api.urls')),
891+ url(r'^', include('dashboard.frontend.ci_job.urls')),
892 )
893
894=== modified file 'requirements.txt'
895--- requirements.txt 2013-02-01 11:39:17 +0000
896+++ requirements.txt 2013-02-26 12:48:40 +0000
897@@ -12,4 +12,5 @@
898 python-jenkins==0.2
899 python-openid==2.2.5
900 requests==1.1.0
901+simplejson==3.0.7
902 wsgiref==0.1.2

Subscribers

People subscribed via source and target branches