Browse Source

Add CI Watch, a third-party CI monitoring dashboard

Original Change-Id: I8611f25a6700c6e0c64c3fadf820dbc9adcd5ea5

Change-Id: I847016f87ebd6da559ecd6298c5ad007bc935cb8
changes/95/231595/1
Skyler Berg 4 years ago
parent
commit
0e03bec4b1

+ 176
- 0
LICENSE View File

@@ -0,0 +1,176 @@
1
+
2
+                                 Apache License
3
+                           Version 2.0, January 2004
4
+                        http://www.apache.org/licenses/
5
+
6
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+   1. Definitions.
9
+
10
+      "License" shall mean the terms and conditions for use, reproduction,
11
+      and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+      "Licensor" shall mean the copyright owner or entity authorized by
14
+      the copyright owner that is granting the License.
15
+
16
+      "Legal Entity" shall mean the union of the acting entity and all
17
+      other entities that control, are controlled by, or are under common
18
+      control with that entity. For the purposes of this definition,
19
+      "control" means (i) the power, direct or indirect, to cause the
20
+      direction or management of such entity, whether by contract or
21
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+      outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+      "You" (or "Your") shall mean an individual or Legal Entity
25
+      exercising permissions granted by this License.
26
+
27
+      "Source" form shall mean the preferred form for making modifications,
28
+      including but not limited to software source code, documentation
29
+      source, and configuration files.
30
+
31
+      "Object" form shall mean any form resulting from mechanical
32
+      transformation or translation of a Source form, including but
33
+      not limited to compiled object code, generated documentation,
34
+      and conversions to other media types.
35
+
36
+      "Work" shall mean the work of authorship, whether in Source or
37
+      Object form, made available under the License, as indicated by a
38
+      copyright notice that is included in or attached to the work
39
+      (an example is provided in the Appendix below).
40
+
41
+      "Derivative Works" shall mean any work, whether in Source or Object
42
+      form, that is based on (or derived from) the Work and for which the
43
+      editorial revisions, annotations, elaborations, or other modifications
44
+      represent, as a whole, an original work of authorship. For the purposes
45
+      of this License, Derivative Works shall not include works that remain
46
+      separable from, or merely link (or bind by name) to the interfaces of,
47
+      the Work and Derivative Works thereof.
48
+
49
+      "Contribution" shall mean any work of authorship, including
50
+      the original version of the Work and any modifications or additions
51
+      to that Work or Derivative Works thereof, that is intentionally
52
+      submitted to Licensor for inclusion in the Work by the copyright owner
53
+      or by an individual or Legal Entity authorized to submit on behalf of
54
+      the copyright owner. For the purposes of this definition, "submitted"
55
+      means any form of electronic, verbal, or written communication sent
56
+      to the Licensor or its representatives, including but not limited to
57
+      communication on electronic mailing lists, source code control systems,
58
+      and issue tracking systems that are managed by, or on behalf of, the
59
+      Licensor for the purpose of discussing and improving the Work, but
60
+      excluding communication that is conspicuously marked or otherwise
61
+      designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+      "Contributor" shall mean Licensor and any individual or Legal Entity
64
+      on behalf of whom a Contribution has been received by Licensor and
65
+      subsequently incorporated within the Work.
66
+
67
+   2. Grant of Copyright License. Subject to the terms and conditions of
68
+      this License, each Contributor hereby grants to You a perpetual,
69
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+      copyright license to reproduce, prepare Derivative Works of,
71
+      publicly display, publicly perform, sublicense, and distribute the
72
+      Work and such Derivative Works in Source or Object form.
73
+
74
+   3. Grant of Patent License. Subject to the terms and conditions of
75
+      this License, each Contributor hereby grants to You a perpetual,
76
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+      (except as stated in this section) patent license to make, have made,
78
+      use, offer to sell, sell, import, and otherwise transfer the Work,
79
+      where such license applies only to those patent claims licensable
80
+      by such Contributor that are necessarily infringed by their
81
+      Contribution(s) alone or by combination of their Contribution(s)
82
+      with the Work to which such Contribution(s) was submitted. If You
83
+      institute patent litigation against any entity (including a
84
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+      or a Contribution incorporated within the Work constitutes direct
86
+      or contributory patent infringement, then any patent licenses
87
+      granted to You under this License for that Work shall terminate
88
+      as of the date such litigation is filed.
89
+
90
+   4. Redistribution. You may reproduce and distribute copies of the
91
+      Work or Derivative Works thereof in any medium, with or without
92
+      modifications, and in Source or Object form, provided that You
93
+      meet the following conditions:
94
+
95
+      (a) You must give any other recipients of the Work or
96
+          Derivative Works a copy of this License; and
97
+
98
+      (b) You must cause any modified files to carry prominent notices
99
+          stating that You changed the files; and
100
+
101
+      (c) You must retain, in the Source form of any Derivative Works
102
+          that You distribute, all copyright, patent, trademark, and
103
+          attribution notices from the Source form of the Work,
104
+          excluding those notices that do not pertain to any part of
105
+          the Derivative Works; and
106
+
107
+      (d) If the Work includes a "NOTICE" text file as part of its
108
+          distribution, then any Derivative Works that You distribute must
109
+          include a readable copy of the attribution notices contained
110
+          within such NOTICE file, excluding those notices that do not
111
+          pertain to any part of the Derivative Works, in at least one
112
+          of the following places: within a NOTICE text file distributed
113
+          as part of the Derivative Works; within the Source form or
114
+          documentation, if provided along with the Derivative Works; or,
115
+          within a display generated by the Derivative Works, if and
116
+          wherever such third-party notices normally appear. The contents
117
+          of the NOTICE file are for informational purposes only and
118
+          do not modify the License. You may add Your own attribution
119
+          notices within Derivative Works that You distribute, alongside
120
+          or as an addendum to the NOTICE text from the Work, provided
121
+          that such additional attribution notices cannot be construed
122
+          as modifying the License.
123
+
124
+      You may add Your own copyright statement to Your modifications and
125
+      may provide additional or different license terms and conditions
126
+      for use, reproduction, or distribution of Your modifications, or
127
+      for any such Derivative Works as a whole, provided Your use,
128
+      reproduction, and distribution of the Work otherwise complies with
129
+      the conditions stated in this License.
130
+
131
+   5. Submission of Contributions. Unless You explicitly state otherwise,
132
+      any Contribution intentionally submitted for inclusion in the Work
133
+      by You to the Licensor shall be under the terms and conditions of
134
+      this License, without any additional terms or conditions.
135
+      Notwithstanding the above, nothing herein shall supersede or modify
136
+      the terms of any separate license agreement you may have executed
137
+      with Licensor regarding such Contributions.
138
+
139
+   6. Trademarks. This License does not grant permission to use the trade
140
+      names, trademarks, service marks, or product names of the Licensor,
141
+      except as required for reasonable and customary use in describing the
142
+      origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+   7. Disclaimer of Warranty. Unless required by applicable law or
145
+      agreed to in writing, Licensor provides the Work (and each
146
+      Contributor provides its Contributions) on an "AS IS" BASIS,
147
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+      implied, including, without limitation, any warranties or conditions
149
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+      PARTICULAR PURPOSE. You are solely responsible for determining the
151
+      appropriateness of using or redistributing the Work and assume any
152
+      risks associated with Your exercise of permissions under this License.
153
+
154
+   8. Limitation of Liability. In no event and under no legal theory,
155
+      whether in tort (including negligence), contract, or otherwise,
156
+      unless required by applicable law (such as deliberate and grossly
157
+      negligent acts) or agreed to in writing, shall any Contributor be
158
+      liable to You for damages, including any direct, indirect, special,
159
+      incidental, or consequential damages of any character arising as a
160
+      result of this License or out of the use or inability to use the
161
+      Work (including but not limited to damages for loss of goodwill,
162
+      work stoppage, computer failure or malfunction, or any and all
163
+      other commercial damages or losses), even if such Contributor
164
+      has been advised of the possibility of such damages.
165
+
166
+   9. Accepting Warranty or Additional Liability. While redistributing
167
+      the Work or Derivative Works thereof, You may choose to offer,
168
+      and charge a fee for, acceptance of support, warranty, indemnity,
169
+      or other liability obligations and/or rights consistent with this
170
+      License. However, in accepting such obligations, You may act only
171
+      on Your own behalf and on Your sole responsibility, not on behalf
172
+      of any other Contributor, and only if You agree to indemnify,
173
+      defend, and hold each Contributor harmless for any liability
174
+      incurred by, or claims asserted against, such Contributor by reason
175
+      of your accepting any such warranty or additional liability.
176
+

+ 53
- 0
README.md View File

@@ -0,0 +1,53 @@
1
+# CI Watch
2
+
3
+## Installation
4
+
5
+From this folder, run the following commands.
6
+
7
+```
8
+pip install -r requirements.txt
9
+pip install -e .
10
+```
11
+
12
+These instructions are for development and testing installations.
13
+
14
+## Usage
15
+
16
+At the moment, this package provides three commands.
17
+
18
+`ci-watch-server`.
19
+Launch a development server.
20
+
21
+`ci-watch-stream-events`.
22
+Stream events from Gerrit and append valid events to `third-party-ci.log`.
23
+
24
+`ci-watch-populate-database`.
25
+Add all entries from `third-party-ci.log` to the database.
26
+
27
+
28
+## Configuration
29
+
30
+Configuration is stored in the `ci-watch.conf` file. Importantly, you can
31
+specify a directory to store the `third-party-ci.log` file (data\_dir) as well
32
+as the database to connect to. Look at `ci-watch.conf.sample` for an example.
33
+
34
+Other settings should be self explanatory based on the provided configuration
35
+file.
36
+
37
+## State of the project
38
+
39
+This project is a work in progress and the code is pretty rough in some places.
40
+
41
+## TODO
42
+
43
+* Add tests.
44
+* Use a different cache other than SimpleCache. It is not threadsafe. We
45
+  should use something like redis instead.
46
+
47
+These items are far from the only work needed for this project.
48
+
49
+
50
+## Acknowledgements
51
+
52
+This code was originally forked from John Griffith's sos-ci project. Some of it
53
+can still be found in the code and configuration file.

+ 18
- 0
ci-watch.conf.sample View File

@@ -0,0 +1,18 @@
1
+[AccountInfo]
2
+gerrit_ssh_key = /path/to/private/key
3
+gerrit_username = your_username
4
+gerrit_host = review.openstack.org
5
+gerrit_port = 29418
6
+
7
+[Data]
8
+debug = True
9
+data_dir = /var/data
10
+
11
+[database]
12
+connection = sqlite:///:memory
13
+
14
+[misc]
15
+; This is a more complete list that is not missing any CI's from the wiki
16
+; It may include projects that don't actually do the third party CI thing
17
+; projects = cinder,nova,swift,rally,murano,keystone,ironic,octavia,os-brick,neutron,tempest,neutron-lbaas,devstack,designate,manila
18
+projects = cinder,nova,swift,rally,murano,ironic,octavia,os-brick,neutron,neutron-lbaas,devstack,manila

+ 17
- 0
ciwatch.wsgi View File

@@ -0,0 +1,17 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import sys
16
+sys.path.insert(0, "/var/www/ciwatch")
17
+from ciwatch import app as application

+ 31
- 0
ciwatch/__init__.py View File

@@ -0,0 +1,31 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from flask import Flask
16
+
17
+app = Flask(__name__)
18
+
19
+from ciwatch import views  # noqa
20
+from ciwatch import filters  # noqa
21
+
22
+
23
+__version__ = "0.0.1"
24
+
25
+
26
+def main():
27
+    app.run(debug=True)
28
+
29
+
30
+if __name__ == '__main__':
31
+    main()

+ 87
- 0
ciwatch/api.py View File

@@ -0,0 +1,87 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from collections import OrderedDict
16
+from datetime import datetime, timedelta
17
+
18
+from flask import request
19
+from sqlalchemy import and_
20
+from sqlalchemy import desc
21
+
22
+from ciwatch import db
23
+from ciwatch.models import CiServer, Project, PatchSet
24
+
25
+
26
+TIME_OPTIONS = OrderedDict([  # Map time options to hours
27
+    ("24 hours", 24),
28
+    ("48 hours", 48),
29
+    ("7 days", 7 * 24),
30
+])
31
+
32
+DEFAULT_TIME_OPTION = "24 hours"
33
+DEFAULT_PROJECT = "cinder"
34
+
35
+
36
+def _get_ci_info_for_patch_sets(ci, patch_sets):
37
+    ci_info = {"name": ci.name, "trusted": ci.trusted, "results": []}
38
+    for patch_set in patch_sets:
39
+        for comment in patch_set.comments:
40
+            if comment.ci_server_id == ci.id:
41
+                ci_info["results"].append(comment)
42
+                break
43
+        else:  # nobreak
44
+            ci_info["results"].append(None)
45
+    return ci_info
46
+
47
+
48
+def get_projects():
49
+    return db.session.query(Project).order_by(Project.name).all()
50
+
51
+
52
+def get_ci_servers():
53
+    return db.session.query(CiServer).order_by(
54
+        desc(CiServer.trusted), CiServer.name).all()
55
+
56
+
57
+def get_patch_sets(project, since):
58
+    return db.session.query(PatchSet).filter(
59
+        and_(PatchSet.project == project, PatchSet.created >= since)
60
+        ).order_by(PatchSet.created.desc()).all()
61
+
62
+
63
+def get_time_options():
64
+    return TIME_OPTIONS.keys()
65
+
66
+
67
+def get_context():
68
+    project = request.args.get('project', DEFAULT_PROJECT)
69
+    time = request.args.get('time', DEFAULT_TIME_OPTION)
70
+    since = datetime.now() - timedelta(hours=TIME_OPTIONS[time])
71
+    project = db.session.query(Project).filter(
72
+        Project.name == project).one()
73
+    patch_sets = get_patch_sets(project=project, since=since)
74
+    results = OrderedDict()
75
+    for ci in get_ci_servers():
76
+        ci_info = _get_ci_info_for_patch_sets(ci, patch_sets)
77
+        if any(result for result in ci_info["results"]):
78
+            results[ci.ci_owner] = results.get(ci.ci_owner, [])
79
+            results[ci.ci_owner].append(
80
+                _get_ci_info_for_patch_sets(ci, patch_sets))
81
+
82
+    return {"time_options": get_time_options(),
83
+            "time_option": time,
84
+            "patch_sets": patch_sets,
85
+            "project": project,
86
+            "projects": get_projects(),
87
+            "user_results": results}

+ 38
- 0
ciwatch/cache.py View File

@@ -0,0 +1,38 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from functools import wraps
16
+
17
+from flask import request
18
+from werkzeug.contrib.cache import SimpleCache
19
+
20
+
21
+cache = SimpleCache()
22
+
23
+
24
+def cached(func):
25
+    @wraps(func)
26
+    def wrapper(*args, **kwargs):
27
+        key = _get_cache_key()
28
+        result = cache.get(key)
29
+        if result is None:
30
+            result = func(*args, **kwargs)
31
+            cache.set(key, result, timeout=60)
32
+        return result
33
+    return wrapper
34
+
35
+
36
+def _get_cache_key():
37
+    args = request.args
38
+    return request.path + str([(key, args[key]) for key in sorted(args)])

+ 28
- 0
ciwatch/config.py View File

@@ -0,0 +1,28 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import os
16
+
17
+from iniparse import INIConfig
18
+
19
+_fdir = os.path.dirname(os.path.realpath(__file__))
20
+_conf_dir = os.path.dirname(_fdir)
21
+cfg = INIConfig(open(_conf_dir + '/ci-watch.conf'))
22
+
23
+
24
+def get_projects():
25
+    projects = []
26
+    for name in cfg.misc.projects.split(','):
27
+        projects.append(name)
28
+    return projects

+ 57
- 0
ciwatch/db.py View File

@@ -0,0 +1,57 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from sqlalchemy import create_engine
16
+from sqlalchemy.orm import sessionmaker
17
+
18
+from ciwatch import models
19
+from ciwatch.config import cfg, get_projects
20
+
21
+
22
+engine = create_engine(cfg.database.connection)
23
+Session = sessionmaker()
24
+Session.configure(bind=engine)
25
+models.Base.metadata.create_all(engine)
26
+session = Session()
27
+
28
+
29
+def create_projects():
30
+    for name in get_projects():
31
+        get_or_create(models.Project,
32
+                      commit_=False,
33
+                      name=name)
34
+    session.commit()
35
+
36
+
37
+def update_or_create_comment(commit_=True, **kwargs):
38
+    comment = session.query(models.Comment).filter_by(
39
+        ci_server_id=kwargs['ci_server_id'],
40
+        patch_set_id=kwargs['patch_set_id']).scalar()
41
+    if comment is not None:
42
+        for key, value in kwargs.iteritems():
43
+            setattr(comment, key, value)
44
+    else:
45
+        session.add(models.Comment(**kwargs))
46
+    if commit_:
47
+        session.commit()
48
+
49
+
50
+def get_or_create(model, commit_=True, **kwargs):
51
+    result = session.query(model).filter_by(**kwargs).first()
52
+    if not result:
53
+        result = model(**kwargs)
54
+        session.add(result)
55
+        if commit_:
56
+            session.commit()
57
+    return result

+ 174
- 0
ciwatch/events.py View File

@@ -0,0 +1,174 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import json
16
+import paramiko
17
+import time
18
+from datetime import datetime
19
+import re
20
+
21
+from ciwatch import db, models
22
+from ciwatch.log import logger, DATA_DIR
23
+from ciwatch.config import cfg, get_projects
24
+
25
+
26
+def _process_project_name(project_name):
27
+    return project_name.split('/')[-1]
28
+
29
+
30
+def _process_event(event):
31
+    comment = event['comment']
32
+    # Find all the CIs voting in this comment
33
+    lines = comment.splitlines()
34
+    event['ci-status'] = {}
35
+    for line in lines:
36
+        possible_results = "FAILURE|SUCCESS|NOT_REGISTERED|UNSTABLE"
37
+        pattern = re.compile("[-*]\s+([^\s*]+)\s+(http[^\s*]+) : (%s)" %
38
+                             possible_results)
39
+        match = pattern.search(line)
40
+        if match is not None:
41
+            ci_name = match.group(1)
42
+            log_url = match.group(2)
43
+            result = match.group(3)
44
+            event['ci-status'][ci_name] = {
45
+                "result": result,
46
+                "log_url": log_url}
47
+
48
+
49
+def _is_ci_user(name):
50
+    return 'CI' in name or 'Jenkins' in name
51
+
52
+
53
+# Check if this is a third party CI event
54
+def _is_valid(event):
55
+    if (event.get('type', 'nill') == 'comment-added' and
56
+            _is_ci_user(event['author'].get('name', '')) and
57
+            _process_project_name(event['change']['project']) in get_projects() and
58
+            event['change']['branch'] == 'master'):
59
+        return True
60
+    return False
61
+
62
+
63
+def _store_event(event):
64
+    with open(DATA_DIR + '/third-party-ci.log', 'a') as f:
65
+        json.dump(event, f)
66
+        f.write('\n')
67
+    add_event_to_db(event)
68
+    return event
69
+
70
+
71
+class GerritEventStream(object):
72
+    def __init__(self):
73
+
74
+        logger.debug('Connecting to %(host)s:%(port)d as '
75
+                     '%(user)s using %(key)s',
76
+                     {'user': cfg.AccountInfo.gerrit_username,
77
+                      'key': cfg.AccountInfo.gerrit_ssh_key,
78
+                      'host': cfg.AccountInfo.gerrit_host,
79
+                      'port': int(cfg.AccountInfo.gerrit_port)})
80
+
81
+        self.ssh = paramiko.SSHClient()
82
+        self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
83
+
84
+        connected = False
85
+        while not connected:
86
+            try:
87
+                self.ssh.connect(cfg.AccountInfo.gerrit_host,
88
+                                 int(cfg.AccountInfo.gerrit_port),
89
+                                 cfg.AccountInfo.gerrit_username,
90
+                                 key_filename=cfg.AccountInfo.gerrit_ssh_key)
91
+                connected = True
92
+            except paramiko.SSHException as e:
93
+                logger.error('%s', e)
94
+                logger.warn('Gerrit may be down, will pause and retry...')
95
+                time.sleep(10)
96
+
97
+        self.stdin, self.stdout, self.stderr =\
98
+            self.ssh.exec_command("gerrit stream-events")
99
+
100
+    def __iter__(self):
101
+        return self
102
+
103
+    def next(self):
104
+        return self.stdout.readline()
105
+
106
+
107
+def parse_json_event(event):
108
+    try:
109
+        event = json.loads(event)
110
+    except Exception as ex:
111
+        logger.error('Failed json.loads on event: %s', event)
112
+        logger.exception(ex)
113
+        return None
114
+    if _is_valid(event):
115
+        _process_event(event)
116
+        logger.info('Parsed valid event: %s', event)
117
+        return event
118
+    return None
119
+
120
+
121
+def add_event_to_db(event, commit_=True):
122
+    project = db.session.query(models.Project).filter(
123
+        models.Project.name == _process_project_name(
124
+            event["change"]["project"])).one()
125
+    patch_set = db.get_or_create(
126
+        models.PatchSet,
127
+        commit_=False,
128
+        project_id=project.id,
129
+        ref=event['patchSet']['ref'],
130
+        commit_message=event['change']['commitMessage'],
131
+        created=datetime.fromtimestamp(
132
+            int(event['patchSet']['createdOn'])))
133
+
134
+    owner_name = event["author"]["name"]
135
+    owner = db.get_or_create(models.CiOwner, name=owner_name)
136
+    trusted = (event["author"]["username"] == "jenkins")
137
+
138
+    if trusted and "approvals" in event:
139
+        if event["approvals"][0]["value"] in ("+1", "+2"):
140
+            patch_set.verified = True
141
+        elif event["approvals"][0]["value"] in ("-1", "-2"):
142
+            patch_set.verified = False
143
+
144
+    for ci, data in event['ci-status'].iteritems():
145
+        ci_server = db.get_or_create(models.CiServer,
146
+                                     commit_=False,
147
+                                     name=ci,
148
+                                     trusted=trusted,
149
+                                     ci_owner_id=owner.id)
150
+        db.update_or_create_comment(commit_=False,
151
+                                    result=data["result"],
152
+                                    log_url=data["log_url"],
153
+                                    ci_server_id=ci_server.id,
154
+                                    patch_set_id=patch_set.id)
155
+    if commit_:
156
+        db.session.commit()
157
+
158
+
159
+def main():
160
+    db.create_projects()  # This will make sure the database has projects in it
161
+    while True:
162
+        try:
163
+            events = GerritEventStream()
164
+        except paramiko.SSHException as ex:
165
+            logger.exception('Error connecting to Gerrit: %s', ex)
166
+            time.sleep(60)
167
+        for event in events:
168
+            event = parse_json_event(event)
169
+            if event is not None:
170
+                _store_event(event)
171
+
172
+
173
+if __name__ == '__main__':
174
+    main()

+ 32
- 0
ciwatch/filters.py View File

@@ -0,0 +1,32 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import re
16
+
17
+from jinja2 import evalcontextfilter, Markup, escape
18
+
19
+from ciwatch import app
20
+
21
+
22
+_paragraph_re = re.compile(r'(?:\r\n|\r|\n){2,}')
23
+
24
+
25
+@app.template_filter()
26
+@evalcontextfilter
27
+def nl2br(eval_ctx, value):
28
+    result = u'\n\n'.join(u'<p>%s</p>' % p.replace('\n', '<br>\n')
29
+                          for p in _paragraph_re.split(escape(value)))
30
+    if eval_ctx.autoescape:
31
+        result = Markup(result)
32
+    return result

+ 46
- 0
ciwatch/log.py View File

@@ -0,0 +1,46 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import logging
16
+from logging import handlers
17
+import os
18
+
19
+from ciwatch.config import cfg
20
+
21
+
22
+def setup_logger(name):
23
+
24
+    logger = logging.getLogger(name)
25
+    logger.setLevel(logging.DEBUG)
26
+    log_formatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s]"
27
+                                      "  %(message)s")
28
+
29
+    file_handler =\
30
+        handlers.RotatingFileHandler(name,
31
+                                     maxBytes=1048576,
32
+                                     backupCount=2,)
33
+    logger.addHandler(file_handler)
34
+
35
+    console_handler = logging.StreamHandler()
36
+    console_handler.setFormatter(log_formatter)
37
+    logger.addHandler(console_handler)
38
+    return logger
39
+
40
+
41
+DATA_DIR =\
42
+    os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/data'
43
+if cfg.Data.data_dir:
44
+    DATA_DIR = cfg.Data.data_dir
45
+
46
+logger = setup_logger(DATA_DIR + '/ci-watch.log')

+ 98
- 0
ciwatch/models.py View File

@@ -0,0 +1,98 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from sqlalchemy import Boolean, Column, DateTime, Integer, String, ForeignKey
16
+from sqlalchemy.ext.declarative import declarative_base
17
+from sqlalchemy.orm import relationship, backref
18
+
19
+
20
+Base = declarative_base()
21
+
22
+
23
+class Project(Base):
24
+    __tablename__ = "projects"
25
+
26
+    id = Column(Integer, primary_key=True)
27
+    name = Column(String, unique=True)
28
+
29
+    def __repr__(self):
30
+        return "<Project(name='%s')>" % self.name
31
+
32
+
33
+class PatchSet(Base):
34
+    __tablename__ = "patch_sets"
35
+
36
+    id = Column(Integer, primary_key=True)
37
+    created = Column(DateTime)
38
+    # ref = Column(String, unique=True)
39
+    ref = Column(String)  # Why are there duplicate refs?
40
+    # Verified only represents Jenkin's vote
41
+    verified = Column(Boolean, nullable=True, default=None)
42
+
43
+    commit_message = Column(String)
44
+
45
+    project_id = Column(Integer, ForeignKey('projects.id'))
46
+    project = relationship("Project", backref=backref('patch_sets',
47
+                                                      order_by=id))
48
+
49
+    def __repr__(self):
50
+        return "<PatchSet(created='%s', ref='%s')>" % (
51
+            self.created, self.ref)
52
+
53
+
54
+class Comment(Base):
55
+    __tablename__ = "comments"
56
+
57
+    id = Column(Integer, primary_key=True)
58
+    result = Column(String)
59
+    log_url = Column(String, nullable=True, default=None)
60
+
61
+    ci_server_id = Column(Integer, ForeignKey('ci_servers.id'))
62
+    ci_server = relationship("CiServer", backref=backref('comments',
63
+                                                         order_by=id))
64
+
65
+    patch_set_id = Column(Integer, ForeignKey('patch_sets.id'))
66
+    patch_set = relationship("PatchSet", backref=backref('comments',
67
+                                                         order_by=id))
68
+
69
+    def __repr__(self):
70
+        return "<Comment(log_url='%s', result='%s')>" % (
71
+            self.log_url, self.result)
72
+
73
+
74
+class CiServer(Base):
75
+    __tablename__ = "ci_servers"
76
+
77
+    id = Column(Integer, primary_key=True)
78
+    name = Column(String)
79
+
80
+    # Official OpenStack CIs are trusted (e.g., Jenkins)
81
+    trusted = Column(Boolean, default=False)
82
+
83
+    ci_owner_id = Column(Integer, ForeignKey('ci_owners.id'))
84
+    ci_owner = relationship('CiOwner', backref=backref('ci_servers',
85
+                                                       order_by=id))
86
+
87
+    def __repr__(self):
88
+        return "<CiServer(name='%s')>" % self.name
89
+
90
+
91
+class CiOwner(Base):
92
+    __tablename__ = "ci_owners"
93
+
94
+    id = Column(Integer, primary_key=True)
95
+    name = Column(String, unique=True)
96
+
97
+    def __repr__(self):
98
+        return "<CiOwner(name='%s')>" % self.name

+ 42
- 0
ciwatch/populate.py View File

@@ -0,0 +1,42 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from ciwatch import db
16
+from ciwatch.events import parse_json_event, add_event_to_db
17
+
18
+
19
+def get_data():
20
+    data = []
21
+    with open('/var/data/third-party-ci.log') as file_:
22
+        for line in file_:
23
+            event = parse_json_event(line)
24
+            if event is not None:
25
+                data.append(event)
26
+    return data
27
+
28
+
29
+def load_data():
30
+    data = get_data()
31
+    for event in data:
32
+        add_event_to_db(event, commit_=False)
33
+    db.session.commit()
34
+
35
+
36
+def main():
37
+    db.create_projects()
38
+    load_data()
39
+
40
+
41
+if __name__ == '__main__':
42
+    main()

+ 23
- 0
ciwatch/static/hover.js View File

@@ -0,0 +1,23 @@
1
+// Code adapted from https://css-tricks.com/row-and-column-highlighting/
2
+$(document).ready(function () {
3
+  $("table").delegate("td.result","mouseover mouseleave", function(e) {
4
+      if (e.type == "mouseover") {
5
+        $(this).parent().addClass("hover");
6
+        $("colgroup").eq($(this).index()).addClass("hover");
7
+      }
8
+      else {
9
+        $(this).parent().removeClass("hover");
10
+        $("colgroup").eq($(this).index()).removeClass("hover");
11
+      }
12
+  });
13
+  $("table").delegate("td.ci-name","mouseover mouseleave", function(e) {
14
+      if (e.type == "mouseover") {
15
+        $(this).parent().addClass("hover");
16
+      }
17
+      else {
18
+        $(this).parent().removeClass("hover");
19
+      }
20
+  });
21
+
22
+  $('[data-toggle="popover"]').popover({trigger: 'hover'});
23
+});

+ 65
- 0
ciwatch/static/style.css View File

@@ -0,0 +1,65 @@
1
+table.table {
2
+  white-space: nowrap;
3
+}
4
+
5
+.hover {
6
+  background-color: #EEE;
7
+}
8
+
9
+.failure {
10
+  color: #C00;
11
+  font-weight: bold;
12
+}
13
+
14
+.success {
15
+  color: #0C0;
16
+  font-weight: bold;
17
+}
18
+
19
+.unstable {
20
+  color: #BB0;
21
+  font-weight: bold;
22
+}
23
+
24
+.unregistered {
25
+  color: #333;
26
+  font-weight: bold;
27
+}
28
+
29
+.ci-user {
30
+  background-color: #DDD !important;
31
+  font-style: italic;
32
+}
33
+
34
+.blank-row {
35
+  height: 20px;
36
+}
37
+
38
+.popover {
39
+  max-width: 100%;
40
+}
41
+
42
+.no-style-link:link {
43
+  text-decoration: none;
44
+  color: #333;
45
+}
46
+
47
+.no-style-link:visited {
48
+  text-decoration: none;
49
+  color: #333;
50
+}
51
+
52
+.no-style-link:hover {
53
+  text-decoration: none;
54
+  color: #333;
55
+}
56
+
57
+.no-style-link:active {
58
+  text-decoration: none;
59
+  color: #333;
60
+}
61
+
62
+.glyphicon-none:before {
63
+  content: "\2122";
64
+  color: transparent !important;
65
+}

+ 33
- 0
ciwatch/static/verified.js View File

@@ -0,0 +1,33 @@
1
+/* Copyright (c) 2015 Tintri. All rights reserved.
2
+ *
3
+ *    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+ *    not use this file except in compliance with the License. You may obtain
5
+ *    a copy of the License at
6
+ *
7
+ *         http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ *    Unless required by applicable law or agreed to in writing, software
10
+ *    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+ *    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+ *    License for the specific language governing permissions and limitations
13
+ *    under the License.
14
+ */
15
+
16
+var toggle1 = function () {
17
+  $(".verified-1").css("background-color", "#FAA");
18
+  $(this).one("click", toggle2);
19
+}
20
+
21
+var toggle2 = function () {
22
+  $(".verified-1").css("background-color", "");
23
+  $(this).one("click", toggle1);
24
+}
25
+
26
+$(document).ready(function () {
27
+  $("colgroup").each(function (i, elem) {
28
+    if ($(elem).hasClass("verified-1")) {
29
+      $("#results").find("td").filter(":nth-child(" + (i + 1) + ")").addClass("verified-1");
30
+    }
31
+  });
32
+  $("#verified-1-button").one("click", toggle1);
33
+});

+ 1
- 0
ciwatch/templates/_contact.html.jinja View File

@@ -0,0 +1 @@
1
+<p>Send feedback to <a href="mailto:openstack-dev@tintri.com">openstack-dev.tintri.com</a></p>

+ 3
- 0
ciwatch/templates/_header.html.jinja View File

@@ -0,0 +1,3 @@
1
+<div class="page-header">
2
+  <h1><a class="no-style-link" href="{{ url_for("home") }}">CI Watch</a><small> &mdash; an OpenStack third-party CI monitoring tool</small></h1>
3
+</div>

+ 7
- 0
ciwatch/templates/_usage.html.jinja View File

@@ -0,0 +1,7 @@
1
+      <p>
2
+        Each project has a table of recent CI results.
3
+        Columns plot patch sets and rows plot CI servers.
4
+        Each cell shows results for the row's CI server and the column's patch set.
5
+        Click the icons to view relevant logs.
6
+        Newest results are shown furthest to the left.
7
+      </p>

+ 111
- 0
ciwatch/templates/index.html.jinja View File

@@ -0,0 +1,111 @@
1
+<html>
2
+  <head>
3
+
4
+    <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
5
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
6
+
7
+    <link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
8
+  </head>
9
+
10
+  <body>
11
+
12
+    <div class="container">
13
+
14
+      {% include '_header.html.jinja' %}
15
+
16
+      <div class="jumbotron">
17
+        <h2>Select a project to view CI results</h2>
18
+        <br>
19
+        <div class="row">
20
+          {% for project in projects %}
21
+          <div class="col-xs-6 col-md-3">
22
+            <a href="{{ url_for('project', project=project.name) }}" class="thumbnail">
23
+              <p style="text-align:center">{{ project.name }}</p>
24
+            </a>
25
+          </div>
26
+          {% endfor %}
27
+        </div>
28
+      </div>
29
+
30
+      <h2>Usage</h2>
31
+      {% include '_usage.html.jinja' %}
32
+
33
+      <div class="panel panel-default">
34
+        <div class="panel-heading">Legend</div>
35
+        <table class="table table-bordered">
36
+          <thead>
37
+            <tr>
38
+              <th>Icon</th>
39
+              <th>Meaning</th>
40
+            </tr>
41
+          </thead>
42
+
43
+          <tbody>
44
+            <tr>
45
+              <td>
46
+                <a href="#">
47
+                  <span class="fa fa-external-link"></span>
48
+                </a>
49
+              </td>
50
+              <td>Link to patch set on Gerrit</td>
51
+            </tr>
52
+
53
+            <tr>
54
+              <td>
55
+                <p class="success">
56
+                  <span class="glyphicon glyphicon-ok">
57
+                  </span>
58
+                </p>
59
+              </td>
60
+              <td>CI voted <span class="success">SUCCESS</span> for patch set</p></td>
61
+            </tr>
62
+
63
+            <tr>
64
+              <td>
65
+                <p class="failure">
66
+                  <span class="glyphicon glyphicon-remove">
67
+                  </span>
68
+                </p>
69
+              </td>
70
+              <td>CI voted <span class="failure">FAILURE</span> for patch set</td>
71
+            </tr>
72
+
73
+            <tr>
74
+              <td>
75
+                <p class="unstable">
76
+                  <span class="fa fa-exclamation-triangle">
77
+                  </span>
78
+                </p>
79
+              </td>
80
+              <td>CI voted <span class="unstable">UNSTABLE</span> for patch set</td>
81
+            </tr>
82
+
83
+            <tr>
84
+              <td>
85
+                <p class="unregistered">N</p>
86
+              </td>
87
+              <td>CI voted <span class="unregistered">NOT_REGISTERED</span> patch set</td>
88
+            </tr>
89
+
90
+            <tr>
91
+              <td>
92
+                <span class="glyphicon glyphicon-none">
93
+                </span>
94
+              </td>
95
+              <td>CI has not yet voted on this patch set</td>
96
+            </tr>
97
+
98
+          </tbody>
99
+        </table>
100
+      </div>
101
+
102
+      <h2>Contact Us</h2>
103
+      {% include '_contact.html.jinja' %}
104
+
105
+    </div>
106
+
107
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
108
+    <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
109
+
110
+  </body>
111
+</html>

+ 109
- 0
ciwatch/templates/project.html.jinja View File

@@ -0,0 +1,109 @@
1
+<html>
2
+  <head>
3
+
4
+    <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
5
+    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">
6
+
7
+    <link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
8
+  </head>
9
+  <body>
10
+
11
+    <div class="container">
12
+      {% include '_header.html.jinja' %}
13
+      {% include '_usage.html.jinja' %}
14
+      {% include '_contact.html.jinja' %}
15
+      <div class="alert alert-info">Viewing results for {{ project.name }} from the past {{ request.args.get('time', time_option) }}.</div>
16
+    </div>
17
+
18
+    <div class="btn-group">
19
+      <div class="btn-group">
20
+        <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">Switch project
21
+          <span class="caret"></span>
22
+        </button>
23
+        <ul class="dropdown-menu">
24
+          {% for proj in projects %}
25
+          <li><a href="{{ url_for('project', project=proj.name, time=request.args.get('time', time_option)) }}">{{ proj.name }}</a></li>
26
+          {% endfor %}
27
+        </ul>
28
+      </div>
29
+
30
+      <div class="btn-group">
31
+        <button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">Past {{ time_option }}
32
+          <span class="caret"></span>
33
+        </button>
34
+        <ul class="dropdown-menu">
35
+          {% for option in time_options %}
36
+          <li><a href="{{ url_for('project', project=project.name, time=option) }}">{{ option }}</a></li>
37
+          {% endfor %}
38
+        </ul>
39
+      </div>
40
+
41
+      <button id="verified-1-button" class="btn btn-default" type="button" data-toggle="button">Highlight Jenkins -1 votes</button>
42
+    </div>
43
+
44
+    <br>
45
+
46
+    <div class="table">
47
+      <table id="results" class="table table-bordered table-condensed">
48
+
49
+        <colgroup></colgroup>
50
+        {% for patch_set in patch_sets %}
51
+        {% if patch_set.verified is not none and not patch_set.verfied %}
52
+        <colgroup class="verified-1"></colgroup>
53
+        {% else %}
54
+        <colgroup></colgroup>
55
+        {% endif %}
56
+        {% endfor %}
57
+
58
+        <tbody>
59
+
60
+          <tr>
61
+            <td>Patch Set</td>
62
+            {% for patch_set in patch_sets %}
63
+            <td style="text-align:center;">
64
+              <a href="https://review.openstack.org/#/c/{{ "/".join(patch_set.ref.split("/")[-2:]) }}"
65
+                 data-toggle="popover" data-html="true" data-placement="bottom" title="Commit Message" 
66
+                 data-content="{{ patch_set.commit_message|nl2br }}">
67
+                <span class="fa fa-external-link"></span>
68
+              </a>
69
+            </td>
70
+            {% endfor %}
71
+          </tr>
72
+
73
+          {% for owner, results in user_results.iteritems() %}
74
+          <tr class="ci-user"><td colspan="{{ patch_sets|length + 1}}" class="ci-user">{{ owner.name }}</td></tr>
75
+          {% for ci in results %}
76
+          <tr>
77
+            <td class="ci-name">{{ ci["name"] }}</td>
78
+            {% for comment in ci["results"] %}
79
+              <td class="result">
80
+              {% if comment is not none %}
81
+                <a href="{{ comment.log_url }}">
82
+                {% if comment.result == "SUCCESS" %}
83
+                  <p class="success"><span class="glyphicon glyphicon-ok"></span></p>
84
+                {% elif comment.result == "FAILURE" %}
85
+                  <p class="failure"><span class="glyphicon glyphicon-remove"></span></p>
86
+                {% elif comment.result == "UNSTABLE" %}
87
+                  <p class="unstable"><span class="fa fa-exclamation-triangle"></span></p>
88
+                {% elif comment.result == "NOT_REGISTERED" %}
89
+                  <p class="unregistered">N</p>
90
+                {% endif %}
91
+              </a>
92
+              {% endif %}
93
+            </td>
94
+            {% endfor %}
95
+          </tr>
96
+          {% endfor %}
97
+          <tr class="blank-row"></tr>
98
+          {% endfor %}
99
+        </tbody>
100
+      </table>
101
+    </div>
102
+
103
+    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
104
+    <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
105
+    <script src="{{ url_for('static', filename='hover.js') }}"></script>
106
+    <script src="{{ url_for('static', filename='verified.js') }}"></script>
107
+
108
+  </body>
109
+</html>

+ 38
- 0
ciwatch/views.py View File

@@ -0,0 +1,38 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+from flask import render_template
16
+from sqlalchemy.orm.exc import NoResultFound
17
+from werkzeug.exceptions import abort
18
+
19
+from ciwatch import app
20
+from ciwatch.api import get_context
21
+from ciwatch.api import get_projects
22
+from ciwatch.cache import cached
23
+
24
+
25
+@app.route("/")
26
+@app.route("/index")
27
+@app.route("/home")
28
+def home():
29
+    return render_template('index.html.jinja', projects=get_projects())
30
+
31
+
32
+@app.route("/project")
33
+@cached
34
+def project():
35
+    try:
36
+        return render_template("project.html.jinja", **get_context())
37
+    except NoResultFound:
38
+        abort(404)

+ 4
- 0
requirements.txt View File

@@ -0,0 +1,4 @@
1
+flask
2
+sqlalchemy
3
+iniparse
4
+paramiko

+ 18
- 0
run.py View File

@@ -0,0 +1,18 @@
1
+# Copyright (c) 2015 Tintri. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import ciwatch
16
+
17
+if __name__ == "__main__":
18
+    ciwatch.app.run(debug=True)

+ 41
- 0
setup.py View File

@@ -0,0 +1,41 @@
1
+#!/usr/bin/env python
2
+
3
+# Copyright (c) 2015 Tintri. All rights reserved.
4
+#
5
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+#    not use this file except in compliance with the License. You may obtain
7
+#    a copy of the License at
8
+#
9
+#         http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+#    Unless required by applicable law or agreed to in writing, software
12
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+#    License for the specific language governing permissions and limitations
15
+#    under the License.
16
+
17
+from os.path import join, dirname
18
+
19
+from setuptools import setup
20
+
21
+import ciwatch
22
+
23
+
24
+setup(
25
+    name='ci-watch',
26
+    version=ciwatch.__version__,
27
+    long_description=open(join(dirname(__file__), 'README.md')).read(),
28
+    entry_points={
29
+        'console_scripts': [
30
+            'ci-watch-server = ciwatch:main',
31
+            'ci-watch-populate-database = ciwatch.populate:main',
32
+            'ci-watch-stream-events = ciwatch.events:main',
33
+            ],
34
+        },
35
+    install_requires=[
36
+        "flask",
37
+        "sqlalchemy",
38
+        "iniparse",
39
+        "paramiko",
40
+    ]
41
+)

Loading…
Cancel
Save