Completed
Push — master ( a364e5...2cdc45 )
by Dieter
01:07
created

buildtimetrend.generate_dashboard_config_file()   B

Complexity

Conditions 3

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 3
dl 0
loc 24
rs 8.9714
1
# vim: set expandtab sw=4 ts=4:
2
"""
3
Interface to Keen IO.
4
5
Copyright (C) 2014-2015 Dieter Adriaenssens <[email protected]>
6
7
This file is part of buildtimetrend/python-lib
8
<https://github.com/buildtimetrend/python-lib/>
9
10
This program is free software: you can redistribute it and/or modify
11
it under the terms of the GNU Affero General Public License as published by
12
the Free Software Foundation, either version 3 of the License, or
13
any later version.
14
15
This program is distributed in the hope that it will be useful,
16
but WITHOUT ANY WARRANTY; without even the implied warranty of
17
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
GNU Affero General Public License for more details.
19
20
You should have received a copy of the GNU Affero General Public License
21
along with this program. If not, see <http://www.gnu.org/licenses/>.
22
"""
23
24
from __future__ import division
25
from builtins import str
26
import os
27
from buildtimetrend import logger
28
import copy
29
import keen
30
import requests
31
from keen import scoped_keys
32
from buildtimetrend.settings import Settings
33
from buildtimetrend.tools import check_dict
34
from buildtimetrend.tools import is_list
35
from buildtimetrend.tools import is_string
36
37
38
TIME_INTERVALS = {
39
    'week': {'name': 'week', 'timeframe': 'this_7_days', 'max_age': 600},
40
    'month': {'name': 'month', 'timeframe': 'this_30_days', 'max_age': 600},
41
    'year': {'name': 'year', 'timeframe': 'this_52_weeks', 'max_age': 1800}
42
}
43
KEEN_PROJECT_INFO_NAME = "buildtime_trend"
44
45
46
def keen_has_project_id():
47
    """Check if Keen.io project ID is set."""
48
    if "KEEN_PROJECT_ID" in os.environ or keen.project_id is not None:
49
        return True
50
51
    logger.warning("Keen.io Project ID is not set")
52
    return False
53
54
55
def keen_has_master_key():
56
    """Check if Keen.io Master API key is set."""
57
    if "KEEN_MASTER_KEY" in os.environ or keen.master_key is not None:
58
        return True
59
60
    logger.warning("Keen.io Master API Key is not set")
61
    return False
62
63
64
def keen_has_write_key():
65
    """Check if Keen.io Write Key is set."""
66
    if "KEEN_WRITE_KEY" in os.environ or keen.write_key is not None:
67
        return True
68
69
    logger.warning("Keen.io Write Key is not set")
70
    return False
71
72
73
def keen_has_read_key():
74
    """Check if Keen.io Read key is set."""
75
    if "KEEN_READ_KEY" in os.environ or keen.read_key is not None:
76
        return True
77
78
    logger.warning("Keen.io Read Key is not set")
79
    return False
80
81
82
def keen_is_writable():
83
    """Check if login keys for Keen IO API are set, to allow writing."""
84
    if keen_has_project_id() and keen_has_write_key():
85
        return True
86
87
    logger.warning("Keen.io Write Key is not set")
88
    return False
89
90
91
def keen_is_readable():
92
    """Check if login keys for Keen IO API are set, to allow reading."""
93
    if keen_has_project_id() and keen_has_read_key():
94
        return True
95
96
    logger.warning("Keen.io Read Key is not set")
97
    return False
98
99
100
def keen_io_generate_read_key(repo):
101
    """
102
    Create scoped key for reading only the build-stages related data.
103
104
    Param repo : github repository slug (fe. buildtimetrend/python-lib)
105
    """
106
    if not keen_has_master_key():
107
        logger.warning("Keen.io Read Key was not created,"
108
                       " keen.master_key is not defined.")
109
        return None
110
111
    master_key = keen.master_key or os.environ.get("KEEN_MASTER_KEY")
112
113
    privileges = {
114
        "allowed_operations": ["read"]
115
    }
116
117
    if repo is not None:
118
        privileges["filters"] = [get_repo_filter(repo)]
119
120
    logger.info("Keen.io Read Key is created for %s", repo)
121
    return scoped_keys.encrypt(master_key, privileges)
122
123
124
def keen_io_generate_write_key():
125
    """Create scoped key for write access to Keen.io database."""
126
    if not keen_has_master_key():
127
        logger.warning("Keen.io Write Key was not created,"
128
                       " keen.master_key is not defined.")
129
        return None
130
131
    master_key = keen.master_key or os.environ.get("KEEN_MASTER_KEY")
132
133
    privileges = {
134
        "allowed_operations": ["write"]
135
    }
136
137
    logger.info("Keen.io Write Key is created")
138
    return scoped_keys.encrypt(master_key, privileges)
139
140
141
def send_build_data(build):
142
    """Send build data generated by client to keen.io."""
143
    if keen_is_writable():
144
        logger.info("Sending client build data to Keen.io")
145
        keen_add_event("build_jobs", {"job": build.to_dict()})
146
        keen_add_events("build_stages", build.stages_to_list())
147
148
149
def send_build_data_service(build):
150
    """Send build data generated by service to keen.io."""
151
    if keen_is_writable():
152
        logger.info("Sending service build data to Keen.io")
153
        keen_add_event("build_jobs", {"job": build.to_dict()})
154
        keen_add_events("build_substages", build.stages_to_list())
155
156
157
def keen_add_event(event_collection, payload):
158
    """
159
    Wrapper for keen.add_event(), adds project info.
160
161
    Param event_collection : collection event data is submitted to
162
    Param payload : data that is submitted
163
    """
164
    # add project info to this event
165
    payload = add_project_info_dict(payload)
166
167
    # submit list of events to Keen.io
168
    keen.add_event(event_collection, payload)
169
170
171
def keen_add_events(event_collection, payload):
172
    """
173
    Wrapper for keen.add_events(), adds project info to each event.
174
175
    Param event_collection : collection event data is submitted to
176
    Param payload : array of events that is submitted
177
    """
178
    # add project info to each event
179
    payload = add_project_info_list(payload)
180
181
    # submit list of events to Keen.io
182
    keen.add_events({event_collection: payload})
183
184
185
def add_project_info_dict(payload):
186
    """
187
    Add project info to a dictonary.
188
189
    Param payload: dictonary payload
190
    """
191
    # check if payload is a dictionary, throws an exception if it isn't
192
    check_dict(payload, "payload")
193
194
    payload_as_dict = copy.deepcopy(payload)
195
196
    payload_as_dict[KEEN_PROJECT_INFO_NAME] = Settings().get_project_info()
197
198
    if "job" in payload:
199
        # override project_name, set to build_job repo
200
        if "repo" in payload["job"]:
201
            payload_as_dict[KEEN_PROJECT_INFO_NAME]["project_name"] = \
202
                payload["job"]["repo"]
203
204
        # override timestamp, set to finished_at timestamp
205
        if "finished_at" in payload["job"]:
206
            payload_as_dict["keen"] = {
207
                "timestamp": payload["job"]["finished_at"]["isotimestamp"]
208
            }
209
210
    return payload_as_dict
211
212
213
def add_project_info_list(payload):
214
    """
215
    Add project info to a list of dictionaries.
216
217
    Param payload: list of dictionaries
218
    """
219
    # check if payload is a list, throws an exception if it isn't
220
    is_list(payload, "payload")
221
222
    payload_as_list = []
223
224
    # loop over dicts in payload and add project info to each one
225
    for event_dict in payload:
226
        payload_as_list.append(add_project_info_dict(event_dict))
227
228
    return payload_as_list
229
230
231
def get_dashboard_keen_config(repo):
232
    """
233
    Generate the Keen.io settings for the configuration of the dashboard.
234
235
    The dashboard is Javascript powered HTML file that contains the
236
    graphs generated by Keen.io.
237
    """
238
    # initialise config settings
239
    keen_config = {}
240
241
    if not keen_has_project_id() or not keen_has_master_key():
242
        logger.warning("Keen.io related config settings could not be created,"
243
                       " keen.project_id and/or keen.master_key"
244
                       " are not defined.")
245
        return keen_config
246
247
    # set keen project ID
248
    if keen.project_id is None:
249
        keen_project_id = os.environ["KEEN_PROJECT_ID"]
250
    else:
251
        keen_project_id = keen.project_id
252
    keen_config['projectId'] = str(keen_project_id)
253
254
    # generate read key
255
    read_key = keen_io_generate_read_key(repo)
256
    if read_key is not None:
257
        # convert bytes to string
258
        if isinstance(read_key, bytes):
259
            read_key = read_key.decode('utf-8')
260
261
        keen_config['readKey'] = str(read_key)
262
263
    # return keen config settings
264
    return keen_config
265
266
267
def get_avg_buildtime(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268
    """
269
    Query Keen.io database and retrieve average build time.
270
271
    Parameters :
272
    - repo : repo name (fe. buildtimetrend/service)
273
    - interval : timeframe, possible values : 'week', 'month', 'year',
274
                 anything else defaults to 'week'
275
    """
276
    if repo is None or not keen_is_readable():
277
        return -1
278
279
    interval_data = check_time_interval(interval)
280
281
    try:
282
        return keen.average(
283
            "build_jobs",
284
            target_property="job.duration",
285
            timeframe=interval_data['timeframe'],
286
            max_age=interval_data['max_age'],
287
            filters=[get_repo_filter(repo)]
288
        )
289
    except requests.ConnectionError:
290
        logger.error("Connection to Keen.io API failed")
291
        return -1
292
    except keen.exceptions.KeenApiError as msg:
293
        logger.error("Error in keenio.get_avg_buildtime() : " + str(msg))
294
        return -1
295
296
297
def get_total_build_jobs(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
    """
299
    Query Keen.io database and retrieve total number of build jobs.
300
301
    Parameters :
302
    - repo : repo name (fe. buildtimetrend/service)
303
    - interval : timeframe, possible values : 'week', 'month', 'year',
304
                 anything else defaults to 'week'
305
    """
306
    if repo is None or not keen_is_readable():
307
        return -1
308
309
    interval_data = check_time_interval(interval)
310
311
    try:
312
        return keen.count_unique(
313
            "build_jobs",
314
            target_property="job.job",
315
            timeframe=interval_data['timeframe'],
316
            max_age=interval_data['max_age'],
317
            filters=[get_repo_filter(repo)]
318
        )
319
    except requests.ConnectionError:
320
        logger.error("Connection to Keen.io API failed")
321
        return -1
322
    except keen.exceptions.KeenApiError as msg:
323
        logger.error("Error in keenio.get_total_build_jobs() : " + str(msg))
324
        return -1
325
326
327
def get_passed_build_jobs(repo=None, interval=None):
328
    """
329
    Query Keen.io database and retrieve total number of build jobs that passed.
330
331
    Parameters :
332
    - repo : repo name (fe. buildtimetrend/service)
333
    - interval : timeframe, possible values : 'week', 'month', 'year',
334
                 anything else defaults to 'week'
335
    """
336
    if repo is None or not keen_is_readable():
337
        return -1
338
339
    interval_data = check_time_interval(interval)
340
341
    try:
342
        return keen.count_unique(
343
            "build_jobs",
344
            target_property="job.job",
345
            timeframe=interval_data['timeframe'],
346
            max_age=interval_data['max_age'],
347
            filters=[
348
                get_repo_filter(repo),
349
                {
350
                    "property_name": "job.result",
351
                    "operator": "eq",
352
                    "property_value": "passed"
353
                }
354
            ]
355
        )
356
    except requests.ConnectionError:
357
        logger.error("Connection to Keen.io API failed")
358
        return -1
359
    except keen.exceptions.KeenApiError as msg:
360
        logger.error("Error in keenio.get_passed_build_jobs() : " + str(msg))
361
        return -1
362
363
364
def get_pct_passed_build_jobs(repo=None, interval=None):
365
    """
366
    Calculate percentage of passed build jobs.
367
368
    Parameters :
369
    - repo : repo name (fe. buildtimetrend/service)
370
    - interval : timeframe, possible values : 'week', 'month', 'year',
371
                 anything else defaults to 'week'
372
    """
373
    total_jobs = get_total_build_jobs(repo, interval)
374
    passed_jobs = get_passed_build_jobs(repo, interval)
375
376
    logger.debug("passed/total build jobs : %d/%d", passed_jobs, total_jobs)
377
378
    # calculate percentage if at least one job was executed
379
    # passed is a valid number (not -1)
380
    if total_jobs > 0 and passed_jobs >= 0:
381
        return int(float(passed_jobs) / float(total_jobs) * 100.0)
382
383
    return -1
384
385
386
def get_result_color(value=0, ok_thershold=90, warning_thershold=70):
387
    """
388
    Get color code that corresponds to the result.
389
390
    Parameters:
391
    - value : value to check
392
    - ok_thershold : OK threshold value
393
    - warning_thershold : warning thershold value
394
    """
395
    if not(type(value) in (int, float) and
396
           type(ok_thershold) in (int, float) and
397
           type(warning_thershold) in (int, float)):
398
        return "lightgrey"
399
400
    # check thresholds
401
    if value >= ok_thershold:
402
        return "green"
403
    elif value >= warning_thershold:
404
        return "yellow"
405
    else:
406
        return "red"
407
408
409
def get_total_builds(repo=None, interval=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
410
    """
411
    Query Keen.io database and retrieve total number of builds.
412
413
    Parameters :
414
    - repo : repo name (fe. buildtimetrend/service)
415
    - interval : timeframe, possible values : 'week', 'month', 'year',
416
                 anything else defaults to 'week'
417
    """
418
    if repo is None or not keen_is_readable():
419
        return -1
420
421
    interval_data = check_time_interval(interval)
422
423
    try:
424
        return keen.count_unique(
425
            "build_jobs",
426
            target_property="job.build",
427
            timeframe=interval_data['timeframe'],
428
            max_age=interval_data['max_age'],
429
            filters=[get_repo_filter(repo)]
430
        )
431
    except requests.ConnectionError:
432
        logger.error("Connection to Keen.io API failed")
433
        return -1
434
    except keen.exceptions.KeenApiError as msg:
435
        logger.error("Error in keenio.get_total_builds() : " + str(msg))
436
        return -1
437
438
439
def get_latest_buildtime(repo=None):
440
    """
441
    Query Keen.io database and retrieve buildtime duration of last build.
442
443
    Parameters :
444
    - repo : repo name (fe. buildtimetrend/python-lib)
445
    """
446
    if repo is None or not keen_is_readable():
447
        return -1
448
449
    try:
450
        result = keen.extraction(
451
            "build_jobs",
452
            property_names="job.duration",
453
            latest=1,
454
            filters=[get_repo_filter(repo)]
455
        )
456
    except requests.ConnectionError:
457
        logger.error("Connection to Keen.io API failed")
458
        return -1
459
    except keen.exceptions.KeenApiError as msg:
460
        logger.error("Error in keenio.get_latest_buildtime() : " + str(msg))
461
        return -1
462
463
    if is_list(result) and len(result) > 0 and \
464
            check_dict(result[0], None, ['job']) and \
465
            check_dict(result[0]['job'], None, ['duration']):
466
        return result[0]['job']['duration']
467
468
    return -1
469
470
471
def has_build_id(repo=None, build_id=None):
472
    """
473
    Check if build_id exists in Keen.io database.
474
475
    Parameters :
476
    - repo : repo name (fe. buildtimetrend/python-lib)
477
    - build_id : ID of the build
478
    """
479
    if repo is None or build_id is None:
480
        logger.error("Repo or build_id is not set")
481
        raise ValueError("Repo or build_id is not set")
482
    if not keen_is_readable():
483
        raise SystemError("Keen.io Project ID or API Read Key is not set")
484
485
    try:
486
        count = keen.count(
487
            "build_jobs",
488
            filters=[get_repo_filter(repo), {
489
                "property_name": "job.build",
490
                "operator": "eq",
491
                "property_value": str(build_id)}]
492
        )
493
    except requests.ConnectionError:
494
        logger.error("Connection to Keen.io API failed")
495
        raise SystemError("Connection to Keen.io API failed")
496
    except keen.exceptions.KeenApiError as msg:
497
        logger.error("Error in keenio.has_build_id : " + str(msg))
498
        raise SystemError(msg)
499
500
    return count > 0
501
502
503
def get_all_projects():
504
    """Query Keen.io database and retrieve a list of all projects."""
505
    if not keen_is_readable():
506
        return []
507
508
    try:
509
        result = keen.select_unique(
510
            "build_jobs",
511
            "buildtime_trend.project_name",
512
            max_age=3600 * 24  # cache for 24 hours
513
        )
514
    except requests.ConnectionError:
515
        logger.error("Connection to Keen.io API failed")
516
        return []
517
    except keen.exceptions.KeenApiError as msg:
518
        logger.error("Error in keenio.get_all_projects() : " + str(msg))
519
        return []
520
521
    if type(result) is list:
522
        return result
523
524
    return []
525
526
527
def get_repo_filter(repo=None):
528
    """
529
    Return filter for analysis request.
530
531
    Parameters
532
    - repo : repo slug name, fe. buildtimetrend/python-lib
533
    """
534
    if repo is None:
535
        return None
536
537
    return {
538
        "property_name": "{}.project_name".format(KEEN_PROJECT_INFO_NAME),
539
        "operator": "eq",
540
        "property_value": str(repo)
541
    }
542
543
544
def check_time_interval(interval=None):
545
    """
546
    Check time interval and returns corresponding parameters.
547
548
    Parameters :
549
    - interval : timeframe, possible values : 'week', 'month', 'year',
550
                 anything else defaults to 'week'
551
    """
552
    if is_string(interval):
553
        # convert to lowercase
554
        interval = interval.lower()
555
556
        if interval in TIME_INTERVALS:
557
            return TIME_INTERVALS[interval]
558
559
    return TIME_INTERVALS['week']
560