14 parser = argparse.ArgumentParser()
15 parser.add_argument('--build-requests-url', required=True, help='URL for the build requests JSON API; e.g. https://perf.webkit.org/api/build-requests/build.webkit.org/')
16 parser.add_argument('--build-requests-user', help='The username for Basic Authentication to access the build requests JSON API')
17 parser.add_argument('--build-requests-password', help='The password for Basic Authentication to access the build requests JSON API')
18 parser.add_argument('--slave-name', required=True, help='The slave name used to update the build requets status')
19 parser.add_argument('--slave-password', required=True, help='The slave password used to update the build requets status')
20 parser.add_argument('--buildbot-url', required=True, help='URL for a buildbot builder; e.g. "https://build.webkit.org/"')
21 parser.add_argument('--builder-config-json', required=True, help='The path to a JSON file that specifies which test and platform will be posted to which builder. '
22 'The JSON should contain an array of dictionaries with keys "platform", "test", and "builder" '
23 'with the platform name (e.g. mountainlion), the test path (e.g. ["Parser", "html5-full-render"]), and the builder name (e.g. Apple MountainLion Release (Perf)) as values.')
24 parser.add_argument('--lookback-count', type=int, default=10, help='The number of builds to look back when finding in-progress builds on the buildbot')
25 parser.add_argument('--seconds-to-sleep', type=float, default=120, help='The seconds to sleep between iterations')
26 args = parser.parse_args()
28 configurations = load_config(args.builder_config_json, args.buildbot_url.strip('/'))
29 build_request_auth = {'user': args.build_requests_user, 'password': args.build_requests_password or ''} if args.build_requests_user else None
32 request_updates.update(find_request_updates(configurations, args.lookback_count))
34 print 'Updating the build requests %s...' % ', '.join(map(str, request_updates.keys()))
38 payload = {'buildRequestUpdates': request_updates, 'slaveName': args.slave_name, 'slavePassword': args.slave_password}
39 response = update_and_fetch_build_requests(args.build_requests_url, build_request_auth, payload)
40 root_sets = response.get('rootSets', {})
41 open_requests = response.get('buildRequests', [])
43 for request in filter(lambda request: request['status'] == 'pending', open_requests):
44 config = config_for_request(configurations, request)
45 if len(config['scheduledRequests']) < 1:
46 print "Scheduling the build request %s..." % str(request['id'])
47 schedule_request(config, request, root_sets)
49 request_updates = find_stale_request_updates(configurations, open_requests, request_updates.keys())
51 print "Found stale build requests %s..." % ', '.join(map(str, request_updates.keys()))
53 time.sleep(args.seconds_to_sleep)
56 def load_config(config_json_path, buildbot_url):
57 with open(config_json_path) as config_json:
58 configurations = json.load(config_json)
60 for config in configurations:
61 escaped_builder_name = urllib.quote(config['builder'])
62 config['url'] = '%s/builders/%s/' % (buildbot_url, escaped_builder_name)
63 config['jsonURL'] = '%s/json/builders/%s/' % (buildbot_url, escaped_builder_name)
64 config['scheduledRequests'] = set()
69 def find_request_updates(configurations, lookback_count):
72 for config in configurations:
74 pending_builds = fetch_json(config['jsonURL'] + 'pendingBuilds')
75 scheduled_requests = filter(None, [request_id_from_build(build) for build in pending_builds])
76 for request_id in scheduled_requests:
77 request_updates[request_id] = {'status': 'scheduled', 'url': config['url']}
78 config['scheduledRequests'] = set(scheduled_requests)
79 except (IOError, ValueError) as error:
80 print >> sys.stderr, "Failed to fetch pending builds for %s: %s" % (config['builder'], str(error))
82 for config in configurations:
83 for i in range(1, lookback_count + 1):
87 build = fetch_json(config['jsonURL'] + 'builds/%d' % build_index)
88 request_id = request_id_from_build(build)
92 in_progress = build.get('currentStep')
94 request_updates[request_id] = {'status': 'running', 'url': config['url']}
95 config['scheduledRequests'].discard(request_id)
97 url = config['url'] + 'builds/' + str(build['number'])
98 request_updates[request_id] = {'status': 'failedIfNotCompleted', 'url': url}
99 except urllib2.HTTPError as error:
100 if error.code == 404:
104 except ValueError as error:
107 print >> sys.stderr, "Failed to fetch build %d for %s: %s" % (build_index, config['builder'], str(build_error))
109 return request_updates
112 def update_and_fetch_build_requests(build_requests_url, build_request_auth, payload):
114 response = fetch_json(build_requests_url, payload=json.dumps(payload), auth=build_request_auth)
115 if response['status'] != 'OK':
116 raise ValueError(response['status'])
118 except (IOError, ValueError) as error:
119 print >> sys.stderr, 'Failed to update or fetch build requests at %s: %s' % (build_requests_url, str(error))
123 def find_stale_request_updates(configurations, open_requests, requests_on_buildbot):
125 for request in open_requests:
126 request_id = int(request['id'])
127 should_be_on_buildbot = request['status'] in ('scheduled', 'running')
128 if should_be_on_buildbot and request_id not in requests_on_buildbot:
129 config = config_for_request(configurations, request)
131 request_updates[request_id] = {'status': 'failed', 'url': config['url']}
132 return request_updates
135 def schedule_request(config, request, root_sets):
136 replacements = root_sets.get(request['rootSet'], {})
137 replacements['buildRequest'] = request['id']
140 for property_name, property_value in config['arguments'].iteritems():
141 for key, value in replacements.iteritems():
142 property_value = property_value.replace('$' + key, value)
143 payload[property_name] = property_value
146 urllib2.urlopen(urllib2.Request(config['url'] + 'force'), urllib.urlencode(payload))
147 config['scheduledRequests'].add(request['id'])
148 except (IOError, ValueError) as error:
149 print >> sys.stderr, "Failed to fetch pending builds for %s: %s" % (config['builder'], str(error))
152 def config_for_request(configurations, request):
153 for config in configurations:
154 if config['platform'] == request['platform'] and config['test'] == request['test']:
159 def fetch_json(url, auth={}, payload=None):
160 request = urllib2.Request(url)
162 request.add_header('Authorization', "Basic %s" % base64.encodestring('%s:%s' % (auth['user'], auth['password'])).rstrip('\n'))
163 response = urllib2.urlopen(request, payload).read()
165 return json.loads(response)
166 except ValueError as error:
167 raise ValueError(str(error) + '\n' + response)
170 def property_value_from_build(build, name):
171 for prop in build.get('properties', []):
177 def request_id_from_build(build):
178 job_id = property_value_from_build(build, 'jobid')
179 return int(job_id) if job_id and job_id.isdigit() else None
182 if __name__ == "__main__":