OLD | NEW |
---|---|
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 import datetime | 5 import datetime |
6 import logging | 6 import logging |
7 import jinja2 | |
7 import webapp2 | 8 import webapp2 |
8 | 9 |
9 import app | 10 import app |
10 import base_page | 11 import base_page |
11 import utils | 12 import utils |
12 | 13 |
14 from third_party.BeautifulSoup.BeautifulSoup import BeautifulSoup | |
13 | 15 |
14 class BuildData(object): | 16 class BuildData(object): |
15 """Represents a single build in the waterfall. | 17 """Represents a single build in the waterfall. |
16 | 18 |
17 Not yet used (this backend only renders a console so far). | 19 Not yet used (this backend only renders a console so far). |
18 TODO(agable): Use, and include step-level info. | 20 TODO(agable): Use, and include step-level info. |
19 """ | 21 """ |
20 | 22 |
21 STATUS_ENUM = ( | 23 STATUS_ENUM = ( |
22 'notstarted', | 24 'notstarted', |
23 'running', | 25 'running', |
24 'success', | 26 'success', |
25 'warnings', | 27 'warnings', |
26 'failure', | 28 'failure', |
27 'exception', | 29 'exception', |
28 ) | 30 ) |
29 | 31 |
30 def __init__(self): | 32 def __init__(self): |
31 self.status = 0 | 33 self.status = 0 |
32 | 34 |
33 | 35 |
34 class RowData(object): | 36 class RowData(object): |
35 """Represents a single row of the console. | 37 """Represents a single row of the console. |
36 | 38 |
37 Includes all individual builder statuses. | 39 Includes all individual builder statuses. |
38 """ | 40 """ |
39 | 41 |
40 def __init__(self): | 42 def __init__(self): |
41 self.revision = 0 | 43 self.revision = 0 |
44 self.revlink = None | |
42 self.committer = None | 45 self.committer = None |
43 self.comment = None | 46 self.comment = None |
44 self.details = None | 47 self.details = None |
48 # Per-builder status stored at self.status[master][category][builder]. | |
45 self.status = {} | 49 self.status = {} |
46 self.timestamp = datetime.datetime.now() | 50 self.timestamp = datetime.datetime.now() |
47 | 51 |
48 | 52 |
49 class MergerData(object): | 53 class MergerData(object): |
50 """Persistent data storage class. | 54 """Persistent data storage class. |
51 | 55 |
52 Holds all of the data we have about the last 100 revisions. | 56 Holds all of the data we have about the last 100 revisions. |
53 Keeps it organized and can render it upon request. | 57 Keeps it organized and can render it upon request. |
54 """ | 58 """ |
55 | 59 |
56 def __init__(self): | 60 def __init__(self): |
57 self.SIZE = 100 | 61 self.SIZE = 100 |
62 # Straight list of masters to display. | |
63 self.ordered_masters = app.DEFAULT_MASTERS_TO_MERGE | |
64 # Ordered categories, indexed by master. | |
65 self.ordered_categories = {} | |
66 # Ordered builders, indexed by master and category. | |
67 self.ordered_builders = {} | |
68 self.latest_rev = 0 | |
58 self.rows = {} | 69 self.rows = {} |
59 self.latest_rev = 0 | 70 self.status = {} |
60 self.ordered_builders = [] | |
61 self.failures = {} | 71 self.failures = {} |
62 | 72 |
63 def bootstrap(self): | 73 def bootstrap(self): |
64 """Fills an empty MergerData with 100 rows of data.""" | 74 """Fills an empty MergerData with 100 rows of data.""" |
75 # Populate the categories, masters, status, and failures data. | |
76 for m in self.ordered_masters: | |
77 for d in (self.ordered_builders, | |
78 self.ordered_categories, | |
79 self.status, | |
80 self.failures): | |
81 if m not in d: | |
M-A Ruel
2013/02/08 02:16:05
d.setdefault(m, {})
| |
82 d[m] = {} | |
83 # Get the category data and construct the list of categories | |
84 # for this master. | |
85 category_data = app.get_and_cache_pagedata('%s/console/categories' % m) | |
86 if not category_data['content']: | |
87 category_list = [u'default'] | |
88 else: | |
89 category_soup = BeautifulSoup(category_data['content']) | |
90 category_list = [tag.string.strip() for tag in | |
91 category_soup.findAll('td', 'DevStatus')] | |
92 self.ordered_categories[m] = category_list | |
93 # Get the builder status data. | |
94 builder_data = app.get_and_cache_pagedata('%s/console/summary' % m) | |
95 if not builder_data['content']: | |
96 continue | |
97 builder_soup = BeautifulSoup(builder_data['content']) | |
98 builders_by_category = builder_soup.tr.findAll('td', 'DevSlave', | |
99 recursive=False) | |
100 # Construct the list of builders for this category. | |
101 for i, c in enumerate(self.ordered_categories[m]): | |
102 if c not in self.ordered_builders[m]: | |
103 self.ordered_builders[m][c] = {} | |
104 builder_list = [tag['title'] for tag in | |
105 builders_by_category[i].findAll('a', 'DevSlaveBox')] | |
106 self.ordered_builders[m][c] = builder_list | |
107 # Fill in the status data for all of this master's builders. | |
108 update_status(m, builder_data['content'], self.status) | |
109 # Copy that status data over into the failures dictionary too. | |
110 for c in self.ordered_categories[m]: | |
111 if c not in self.failures[m]: | |
112 self.failures[m][c] = {} | |
113 for b in self.ordered_builders[m][c]: | |
114 if self.status[m][c][b] not in ('success', 'running', 'notstarted'): | |
115 self.failures[m][c][b] = True | |
116 else: | |
117 self.failures[m][c][b] = False | |
118 # Populate the individual row data, saving status info in the same | |
119 # master/category/builder tree format constructed above. | |
65 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) | 120 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) |
66 if not latest_rev: | 121 if not latest_rev: |
67 logging.error("MergerData.bootstrap(): Didn't get latest_rev. Aborting.") | 122 logging.error("MergerData.bootstrap(): Didn't get latest_rev. Aborting.") |
68 return | 123 return |
69 n = latest_rev | 124 n = latest_rev |
70 num_rows_saved = num_rows_skipped = 0 | 125 num_rows_saved = num_rows_skipped = 0 |
71 while num_rows_saved < self.SIZE and num_rows_skipped < 10: | 126 while num_rows_saved < self.SIZE and num_rows_skipped < 10: |
72 logging.info('MergerData.bootstrap(): Getting revision %s' % n) | |
73 curr_row = RowData() | 127 curr_row = RowData() |
74 for m in app.DEFAULT_MASTERS_TO_MERGE: | 128 for m in self.ordered_masters: |
75 # Fetch the relevant data from the datastore / cache. | 129 update_row(n, m, curr_row) |
76 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) | |
77 if not row_data: | |
78 continue | |
79 # Only grab the common data from the main master. | |
80 if m == 'chromium.main': | |
81 curr_row.revision = int(row_data['rev_number']) | |
82 curr_row.committer = row_data['name'] | |
83 curr_row.comment = row_data['comment'] | |
84 curr_row.details = row_data['details'] | |
85 curr_row.status[m] = row_data['status'] | |
86 # If we didn't get any data, that revision doesn't exist, so skip on. | 130 # If we didn't get any data, that revision doesn't exist, so skip on. |
87 if not curr_row.revision: | 131 if not curr_row.revision: |
88 logging.info('MergerData.bootstrap(): No data for revision %s' % n) | |
89 num_rows_skipped += 1 | 132 num_rows_skipped += 1 |
90 n -= 1 | 133 n -= 1 |
91 continue | 134 continue |
92 logging.info('MergerData.bootstrap(): Got data for revision %s' % n) | |
93 self.rows[n] = curr_row | 135 self.rows[n] = curr_row |
94 num_rows_skipped = 0 | 136 num_rows_skipped = 0 |
95 num_rows_saved += 1 | 137 num_rows_saved += 1 |
96 n -= 1 | 138 n -= 1 |
97 self.latest_rev = max(self.rows.keys()) | 139 self.latest_rev = max(self.rows.keys()) |
98 | 140 |
99 | 141 |
142 def update_row(revision, master, row): | |
143 """Fetches a row from the datastore and puts it in a RowData object.""" | |
144 # Fetch the relevant data from the datastore / cache. | |
145 row_data = app.get_and_cache_rowdata('%s/console/%s' % (master, revision)) | |
146 if not row_data: | |
147 return | |
148 # Only grab the common data from the main master. | |
149 if master == 'chromium.main': | |
150 row.revision = int(row_data['rev_number']) | |
151 row.revlink = row_data['rev'] | |
152 row.committer = row_data['name'] | |
153 row.comment = row_data['comment'] | |
154 row.details = row_data['details'] | |
155 if master not in row.status: | |
156 row.status[master] = {} | |
157 update_status(master, row_data['status'], row.status) | |
158 | |
159 | |
160 def update_status(master, status_html, status_dict): | |
161 """Parses build status information and saves it to a status dictionary.""" | |
162 builder_soup = BeautifulSoup(status_html) | |
163 builders_by_category = builder_soup.findAll('table') | |
164 for i, c in enumerate(data.ordered_categories[master]): | |
165 if c not in status_dict[master]: | |
166 status_dict[master][c] = {} | |
167 statuses_by_builder = builders_by_category[i].findAll('td', | |
168 'DevStatusBox') | |
169 # If we didn't get anything, it's because we're parsing the overall | |
170 # summary, so look for Slave boxes instead of Status boxes. | |
171 if not statuses_by_builder: | |
172 statuses_by_builder = builders_by_category[i].findAll('td', | |
173 'DevSlaveBox') | |
174 for j, b in enumerate(data.ordered_builders[master][c]): | |
175 # Save the whole link as the status to keep ETA and build number info. | |
176 status = unicode(statuses_by_builder[j].a) | |
177 status_dict[master][c][b] = status | |
178 | |
179 | |
180 def notstarted(status): | |
181 """Converts a DevSlave status box to a notstarted DevStatus box.""" | |
182 status_soup = BeautifulSoup(status) | |
183 status_soup['class'] = 'DevStatusBox notstarted' | |
184 return unicode(status_soup) | |
185 | |
186 | |
100 class MergerUpdateAction(base_page.BasePage): | 187 class MergerUpdateAction(base_page.BasePage): |
101 """Handles update requests. | 188 """Handles update requests. |
102 | 189 |
103 Takes data gathered by the cronjob and pulls it into active memory. | 190 Takes data gathered by the cronjob and pulls it into active memory. |
104 """ | 191 """ |
105 | 192 |
106 def get(self): | 193 def get(self): |
107 logging.info("***BACKEND MERGER UPDATE***") | |
108 logging.info('BEGIN Stored rows are: %s' % sorted(data.rows)) | |
109 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) | 194 latest_rev = int(app.get_and_cache_rowdata('latest_rev')['rev_number']) |
110 logging.info('Merger.update(): latest_rev = %s' % latest_rev) | |
111 # We may have brand new rows, so store them. | 195 # We may have brand new rows, so store them. |
112 if latest_rev not in data.rows: | 196 if latest_rev not in data.rows: |
113 logging.info('Merger.update(): Handling new rows.') | |
114 for n in xrange(data.latest_rev + 1, latest_rev + 1): | 197 for n in xrange(data.latest_rev + 1, latest_rev + 1): |
115 logging.info('Merger.update(): Getting revision %s' % n) | |
116 curr_row = RowData() | 198 curr_row = RowData() |
117 for m in app.DEFAULT_MASTERS_TO_MERGE: | 199 for m in data.ordered_masters: |
118 # Fetch the relevant data from the datastore / cache. | 200 update_row(n, m, curr_row) |
119 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) | |
120 if not row_data: | |
121 continue | |
122 # Only grab the common data from the main master. | |
123 if m == 'chromium.main': | |
124 curr_row.revision = int(row_data['rev_number']) | |
125 curr_row.committer = row_data['name'] | |
126 curr_row.comment = row_data['comment'] | |
127 curr_row.details = row_data['details'] | |
128 curr_row.status[m] = row_data['status'] | |
129 # If we didn't get any data, that revision doesn't exist, so skip on. | 201 # If we didn't get any data, that revision doesn't exist, so skip on. |
130 if not curr_row.revision: | 202 if not curr_row.revision: |
131 logging.info('Merger.update(): No data for revision %s' % n) | |
132 continue | 203 continue |
133 logging.info('Merger.update(): Got data for revision %s' % n) | |
134 data.rows[n] = curr_row | 204 data.rows[n] = curr_row |
135 # Update our stored latest_rev to reflect the new data. | 205 # Update our stored latest_rev to reflect the new data. |
136 data.latest_rev = max(data.rows.keys()) | 206 data.latest_rev = max(data.rows.keys()) |
137 # Now update the status of the rest of the rows. | 207 # Now update the status of the rest of the rows. |
138 offset = 0 | 208 offset = 0 |
139 logging.info('Merger.update(): Updating rows.') | |
140 while offset < data.SIZE: | 209 while offset < data.SIZE: |
141 n = data.latest_rev - offset | 210 n = data.latest_rev - offset |
142 logging.info('Merger.update(): Checking revision %s' % n) | |
143 if n not in data.rows: | 211 if n not in data.rows: |
144 logging.info('Merger.update(): Don\'t care about revision %s' % n) | |
145 offset += 1 | 212 offset += 1 |
146 continue | 213 continue |
147 curr_row = data.rows[n] | 214 curr_row = data.rows[n] |
148 for m in app.DEFAULT_MASTERS_TO_MERGE: | 215 for m in data.ordered_masters: |
149 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) | 216 row_data = app.get_and_cache_rowdata('%s/console/%s' % (m, n)) |
150 if not row_data: | 217 if not row_data: |
151 continue | 218 continue |
152 curr_row.status[m] = row_data['status'] | 219 update_status(m, row_data['status'], curr_row.status) |
153 offset += 1 | 220 offset += 1 |
154 logging.info('Merger.update(): Got new data for revision %s' % n) | |
155 # Finally delete any extra rows that we don't want to keep around. | 221 # Finally delete any extra rows that we don't want to keep around. |
156 if len(data.rows) > data.SIZE: | 222 if len(data.rows) > data.SIZE: |
157 old_revs = sorted(data.rows, reverse=True)[data.SIZE:] | 223 old_revs = sorted(data.rows.keys(), reverse=True)[data.SIZE:] |
158 logging.info('Merger.update(): Deleting rows %s' % old_revs) | |
159 for rev in old_revs: | 224 for rev in old_revs: |
160 del data.rows[rev] | 225 del data.rows[rev] |
161 logging.info('FINAL Stored rows are: %s' % sorted(data.rows)) | 226 self.response.out.write('Update completed (rows %s - %s).' % |
162 self.response.out.write('Update completed.') | 227 (min(data.rows.keys()), max(data.rows.keys()))) |
163 | 228 |
164 | 229 |
165 class MergerRenderAction(base_page.BasePage): | 230 class MergerRenderAction(base_page.BasePage): |
166 | 231 |
167 def get(self): | 232 def get(self): |
233 class TemplateData(object): | |
234 def __init__(self, rhs, numrevs): | |
235 self.ordered_rows = sorted(rhs.rows.keys(), reverse=True)[:numrevs] | |
236 self.ordered_masters = rhs.ordered_masters | |
237 self.ordered_categories = rhs.ordered_categories | |
238 self.ordered_builders = rhs.ordered_builders | |
239 self.status = rhs.status | |
240 self.rows = {} | |
241 for row in self.ordered_rows: | |
242 self.rows[row] = rhs.rows[row] | |
243 self.category_count = sum([len(self.ordered_categories[master]) | |
244 for master in self.ordered_masters]) | |
168 num_revs = self.request.get('numrevs') | 245 num_revs = self.request.get('numrevs') |
169 if num_revs: | 246 if num_revs: |
170 num_revs = utils.clean_int(num_revs, -1) | 247 num_revs = utils.clean_int(num_revs, -1) |
171 if not num_revs or num_revs <= 0: | 248 if not num_revs or num_revs <= 0: |
172 num_revs = 25 | 249 num_revs = 25 |
173 self.response.out.write('Render not yet implemented (%s rows).' % num_revs) | 250 out = TemplateData(data, num_revs) |
251 template = template_environment.get_template('merger_b.html') | |
252 self.response.out.write(template.render(data=out)) | |
174 | 253 |
175 | 254 |
176 # Summon our persistent data model into existence. | 255 # Summon our persistent data model into existence. |
177 data = MergerData() | 256 data = MergerData() |
178 data.bootstrap() | 257 data.bootstrap() |
258 template_environment = jinja2.Environment() | |
259 template_environment.loader = jinja2.FileSystemLoader('templates') | |
260 template_environment.filters['notstarted'] = notstarted | |
261 | |
179 | 262 |
180 URLS = [ | 263 URLS = [ |
181 ('/restricted/merger/update', MergerUpdateAction), | 264 ('/restricted/merger/update', MergerUpdateAction), |
182 ('/restricted/merger/render.*', MergerRenderAction), | 265 ('/restricted/merger/render.*', MergerRenderAction), |
183 ] | 266 ] |
184 | 267 |
185 application = webapp2.WSGIApplication(URLS, debug=True) | 268 application = webapp2.WSGIApplication(URLS, debug=True) |
OLD | NEW |