~pwlars/ubuntu-test-cases/krillin-recovery

« back to all changes in this revision

Viewing changes to scripts/dashboard.py

  • Committer: Andy Doan
  • Date: 2014-01-07 17:56:16 UTC
  • Revision ID: andy.doan@canonical.com-20140107175616-hazstr3a5x75d021
add ability to optionally update the dashboard in realtime

if the right combination of environment variables are provided to
this script then we'll update the dashboard with realtime status
of a smoke run.

Variables needed by a job to enable this are:

  DASHBOARD_HOST
  DASHBOARD_USER
  DASHBOARD_KEY

Optional values are:

  DASHBOARD_PORT
  DASHBOARD_PREFIX

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
import argparse
 
4
import datetime
 
5
import json
 
6
import logging
 
7
import os
 
8
 
 
9
from httplib import ACCEPTED, HTTPConnection, HTTPException, OK, CREATED
 
10
from urllib import urlencode
 
11
from urlparse import urlparse
 
12
 
 
13
log = logging.getLogger()
 
14
 
 
15
 
 
16
class API(object):
 
17
    def __init__(self, host=None, port=None, user=None, key=None, prefix=None):
 
18
        if not host:
 
19
            host = os.environ.get('DASHBOARD_HOST', None)
 
20
        if not port:
 
21
            port = int(os.environ.get('DASHBOARD_PORT', '80'))
 
22
        if not user:
 
23
            user = os.environ.get('DASHBOARD_USER', None)
 
24
        if not key:
 
25
            key = os.environ.get('DASHBOARD_KEY', None)
 
26
        if not prefix:
 
27
            prefix = os.environ.get('DASHBOARD_PREFIX', None)
 
28
 
 
29
        self.host = host
 
30
        self.port = port
 
31
        self.resource_base = prefix
 
32
 
 
33
        self._headers = None
 
34
        if user and key:
 
35
            self._headers = {
 
36
                'Content-Type': 'application/json',
 
37
                'Authorization': 'ApiKey %s:%s' % (user, key)
 
38
            }
 
39
            # mod_wsgi will strip the Authorization header, but tastypie
 
40
            # allows it as GET param also. More details for fixing apache:
 
41
            #  http://django-tastypie.rtfd.org/en/latest/authentication.html
 
42
            self._auth_param = '?username=%s&api_key=%s' % (user, key)
 
43
 
 
44
    def _connect(self):
 
45
        if self.host:
 
46
            return HTTPConnection(self.host, self.port)
 
47
        return None
 
48
 
 
49
    def _http_get(self, resource):
 
50
        con = self._connect()
 
51
        if not con:
 
52
            # we just mock this for the case where the caller wants to
 
53
            # use our API transparently enabled/disabled
 
54
            return {}
 
55
 
 
56
        if self.resource_base:
 
57
            resource = self.resource_base + resource
 
58
 
 
59
        logging.debug('doing get on: %s', resource)
 
60
        headers = {'Content-Type': 'application/json'}
 
61
        con.request('GET', resource, headers=headers)
 
62
        resp = con.getresponse()
 
63
        if resp.status != OK:
 
64
            msg = ''
 
65
            try:
 
66
                msg = resp.read().decode()
 
67
            except:
 
68
                pass
 
69
            fmt = '%d error getting resource(%s): %s'
 
70
            raise HTTPException(fmt % (resp.status, resource, msg))
 
71
        data = json.loads(resp.read().decode())
 
72
        if len(data['objects']) == 0:
 
73
            raise HTTPException('resource not found: %s' % resource)
 
74
        assert len(data['objects']) == 1
 
75
        return data['objects'][0]['resource_uri']
 
76
 
 
77
    def _http_post(self, resource, params):
 
78
        con = self._connect()
 
79
        if not con or not self._headers:
 
80
            return None
 
81
 
 
82
        if self.resource_base:
 
83
            resource = self.resource_base + resource
 
84
        resource += self._auth_param
 
85
 
 
86
        params = json.dumps(params)
 
87
        log.debug('posting (%s): %s', resource, params)
 
88
        con.request('POST', resource, params, self._headers)
 
89
        resp = con.getresponse()
 
90
        if resp.status != CREATED:
 
91
            msg = ''
 
92
            try:
 
93
                msg = str(resp.getheaders())
 
94
                msg += resp.read().decode()
 
95
            except:
 
96
                pass
 
97
            raise HTTPException(
 
98
                '%d creating resource(%s): %s' % (resp.status, resource, msg))
 
99
        uri = resp.getheader('Location')
 
100
        return urlparse(uri).path
 
101
 
 
102
    def _http_patch(self, resource, params):
 
103
        con = self._connect()
 
104
        if not con or not self._headers:
 
105
            return None
 
106
 
 
107
        resource += self._auth_param
 
108
 
 
109
        con.request('PATCH', resource, json.dumps(params), self._headers)
 
110
        resp = con.getresponse()
 
111
        if resp.status != ACCEPTED:
 
112
            msg = ''
 
113
            try:
 
114
                msg = resp.getheaders()
 
115
            except:
 
116
                pass
 
117
            raise HTTPException(
 
118
                '%d patching resource(%s): %s' % (resp.status, resource, msg))
 
119
        return resource
 
120
 
 
121
    @staticmethod
 
122
    def _uri_to_pk(resource_uri):
 
123
        if resource_uri:
 
124
            return resource_uri.split('/')[-2]
 
125
        return None  # we are mocked
 
126
 
 
127
    def job_get(self, name):
 
128
        resource = '/smokeng/api/v1/job/?' + urlencode({'name': name})
 
129
        return self._http_get(resource)
 
130
 
 
131
    def job_add(self, name):
 
132
        resource = '/smokeng/api/v1/job/'
 
133
        params = {
 
134
            'name': name,
 
135
            'url': 'http://jenkins.qa.ubuntu.com/job/' + name + '/'
 
136
        }
 
137
        return self._http_post(resource, params)
 
138
 
 
139
    def build_add(self, job_name, job_number):
 
140
        try:
 
141
            logging.debug('trying to find job: %s', job_name)
 
142
            job = self.job_get(job_name)
 
143
        except HTTPException:
 
144
            job = self.job_add(job_name)
 
145
        logging.info('job is: %s', job)
 
146
 
 
147
        resource = '/smokeng/api/v1/build/'
 
148
        params = {
 
149
            'build_number': job_number,
 
150
            'job': job,
 
151
            'ran_at': datetime.datetime.now().isoformat(),
 
152
            'build_description': 'inprogress',
 
153
        }
 
154
        return self._http_post(resource, params)
 
155
 
 
156
    def _image_get(self, build_number, release, variant, arch, flavor):
 
157
        resource = '/smokeng/api/v1/image/?'
 
158
        resource += urlencode({
 
159
            'build_number': build_number,
 
160
            'release': release,
 
161
            'flavor': flavor,
 
162
            'variant': variant,
 
163
            'arch': arch,
 
164
        })
 
165
        return self._http_get(resource)
 
166
 
 
167
    def image_add(self, build_number, release, variant, arch, flavor):
 
168
        try:
 
169
            img = self._image_get(build_number, release, variant, arch, flavor)
 
170
            return img
 
171
        except HTTPException:
 
172
            # image doesn't exist so go continue and create
 
173
            pass
 
174
 
 
175
        resource = '/smokeng/api/v1/image/'
 
176
        params = {
 
177
            'build_number': build_number,
 
178
            'release': release,
 
179
            'flavor': flavor,
 
180
            'variant': variant,
 
181
            'arch': arch,
 
182
        }
 
183
        return self._http_post(resource, params)
 
184
 
 
185
    def result_get(self, image, test):
 
186
        # deal with getting resource uri's as parameters instead of id's
 
187
        image = API._uri_to_pk(image)
 
188
 
 
189
        resource = '/smokeng/api/v1/result/?'
 
190
        resource += urlencode({
 
191
            'image': image,
 
192
            'name': test,
 
193
        })
 
194
        return self._http_get(resource)
 
195
 
 
196
    def _result_status(self, image, build, test, status,
 
197
                       passes=0, fails=0, errors=0):
 
198
        create = False
 
199
        params = {
 
200
            'ran_at': datetime.datetime.now().isoformat(),
 
201
            'status': status,
 
202
            'total_count': passes + fails + errors,
 
203
            'pass_count': passes,
 
204
            'error_count': errors,
 
205
            'fail_count': fails,
 
206
            'jenkins_build': build,
 
207
        }
 
208
 
 
209
        try:
 
210
            resource = self.result_get(image, test)
 
211
        except HTTPException:
 
212
            create = True
 
213
            resource = '/smokeng/api/v1/result/'
 
214
            params['image'] = image
 
215
            params['name'] = test
 
216
 
 
217
        if create:
 
218
            return self._http_post(resource, params)
 
219
        else:
 
220
            return self._http_patch(resource, params)
 
221
 
 
222
    def result_queue(self, image, build, test):
 
223
        return self._result_status(image, build, test, 0)
 
224
 
 
225
    def result_running(self, image, build, test):
 
226
        return self._result_status(image, build, test, 1)
 
227
 
 
228
    def result_syncing(self, image, build, test, passes, fails, errors):
 
229
        return self._result_status(
 
230
            image, build, test, 2, passes, fails, errors)
 
231
 
 
232
 
 
233
def _result_running(api, args):
 
234
    return api.result_running(args.image, args.build, args.test)
 
235
 
 
236
 
 
237
def _result_syncing(api, args):
 
238
    passes = args.tests - args.fails - args.errors
 
239
    return api.result_syncing(args.image, args.build, args.test,
 
240
                              passes, args.fails, args.errors)
 
241
 
 
242
 
 
243
def _set_args(parser, names, func):
 
244
    for n in names:
 
245
        parser.add_argument(n, required=True)
 
246
    parser.set_defaults(func=func)
 
247
 
 
248
 
 
249
def _get_parser():
 
250
    parser = argparse.ArgumentParser(
 
251
        description='Interact with the CI dashboard API')
 
252
 
 
253
    sub = parser.add_subparsers(title='Commands', metavar='')
 
254
 
 
255
    args = ['--image', '--build', '--test']
 
256
    p = sub.add_parser('result-running', help='Set a SmokeResult "Running".')
 
257
    _set_args(p, args, _result_running)
 
258
 
 
259
    p = sub.add_parser('result-syncing', help='Set a SmokeResult "Syncing".')
 
260
    _set_args(p, args, _result_syncing)
 
261
    p.add_argument('--tests', type=int, default=0)
 
262
    p.add_argument('--fails', type=int, default=0)
 
263
    p.add_argument('--errors', type=int, default=0)
 
264
 
 
265
    return parser
 
266
 
 
267
 
 
268
def _assert_env():
 
269
    required = [
 
270
        'DASHBOARD_HOST', 'DASHBOARD_PORT', 'DASHBOARD_USER', 'DASHBOARD_KEY']
 
271
    missing = []
 
272
    for r in required:
 
273
        if r not in os.environ:
 
274
            missing.append(r)
 
275
    if len(missing):
 
276
        print('Missing the following environment variables:')
 
277
        for x in missing:
 
278
            print('  %s' % x)
 
279
        exit(1)
 
280
 
 
281
 
 
282
def _main(args):
 
283
    _assert_env()
 
284
 
 
285
    api = API()
 
286
    try:
 
287
        val = args.func(api, args)
 
288
        if val:
 
289
            print(val)
 
290
    except HTTPException as e:
 
291
        print('ERROR: %s' % e)
 
292
        exit(1)
 
293
 
 
294
    exit(0)
 
295
 
 
296
if __name__ == '__main__':
 
297
    args = _get_parser().parse_args()
 
298
    exit(_main(args))