bug 1032978: Add a standalone process that listens to pulse for release related buildbot messages and necessary release runner changes. r=bhearsum
authorJohn Zeller <johnlzeller@gmail.com.
Fri, 05 Sep 2014 09:22:58 -0400
changeset 4889 4000dfccc44136ca6915647e36fb5f3bdc343f27
parent 4888 3819e11d9dfaa5972b3c166400cb92491909c8c1
child 4890 6252eb2a9f5b6a06a4406eeb6c5f0688701a6b88
push id3607
push userbhearsum@mozilla.com
push dateFri, 05 Sep 2014 13:23:07 +0000
reviewersbhearsum
bugs1032978
bug 1032978: Add a standalone process that listens to pulse for release related buildbot messages and necessary release runner changes. r=bhearsum
buildfarm/release/release-runner.py
buildfarm/release/shipit-notifier.ini.example
buildfarm/release/shipit-notifier.py
lib/python/kickoff/api.py
setup.py
--- a/buildfarm/release/release-runner.py
+++ b/buildfarm/release/release-runner.py
@@ -1,15 +1,16 @@
 #!/usr/bin/env python
 
 import site
 import time
 import logging
 import sys
 import os
+import json
 from os import path
 from optparse import OptionParser
 from smtplib import SMTPException
 from functools import partial
 import textwrap
 from twisted.python.lockfile import FilesystemLock
 
 site.addsitedir(path.join(path.dirname(__file__), "../../lib/python"))
@@ -81,31 +82,32 @@ class ReleaseRunner(object):
     def update_status(self, release, status):
         log.info('updating status for %s to %s' % (release['name'], status))
         try:
             self.release_api.update(release['name'], status=status)
         except requests.HTTPError, e:
             log.warning('Caught HTTPError: %s' % e.response.content)
             log.warning('status update failed, continuing...', exc_info=True)
 
-    def start_release_automation(self, release, master):
+    def start_release_automation(self, release, master, enUSPlatforms):
         sendchange(
             release['branch'],
             getReleaseTag(getBaseTag(release['product'],
                                      release['version'])),
             release['submitter'],
             master,
             release['product']
         )
-        self.mark_as_completed(release)
+        self.mark_as_completed(release, enUSPlatforms)
 
-    def mark_as_completed(self, release):
+    def mark_as_completed(self, release, enUSPlatforms):
         log.info('mark as completed %s' % release['name'])
-        self.release_api.update(
-            release['name'], complete=True, status='Started')
+        self.release_api.update(release['name'], complete=True, 
+                                status='Started', 
+                                enUSPlatforms=json.dumps(enUSPlatforms))
 
     def mark_as_failed(self, release, why):
         log.info('mark as failed %s' % release['name'])
         self.release_api.update(release['name'], ready=False, status=why)
 
 
 def getPartials(release):
     partials = {}
@@ -415,17 +417,25 @@ def main(options):
         for release in rr.new_releases:
             rr.mark_as_failed(release, 'Failed: %s' % repr(e))
         raise
 
     rc = 0
     for release in rr.new_releases:
         try:
             rr.update_status(release, 'Running sendchange command')
-            rr.start_release_automation(release, sendchange_master)
+            staging = config.getboolean('release-runner', 'staging')
+            update(configs_workdir, revision='default')
+            cfgFile = path.join(configs_workdir, 
+                                'mozilla', 
+                                getReleaseConfigName(release['product'], 
+                                                     path.basename(release['branch']),
+                                                     release['version'], staging))
+            enUSPlatforms = readReleaseConfig(cfgFile)['enUSPlatforms']
+            rr.start_release_automation(release, sendchange_master, enUSPlatforms)
         except:
             # We explicitly do not raise an error here because there's no
             # reason not to start other releases if the sendchange fails for
             # another one. We _do_ need to set this in order to exit
             # with the right code, though.
             rc = 2
             rr.update_status(release, 'Sendchange failed')
             log.error('Sendchange failed for %s: ' % release, exc_info=True)
new file mode 100644
--- /dev/null
+++ b/buildfarm/release/shipit-notifier.ini.example
@@ -0,0 +1,7 @@
+[api]
+api_root: http://127.0.0.1:5000
+username: admin
+password: password
+
+[shipit-notifier]
+verbose: true
new file mode 100644
--- /dev/null
+++ b/buildfarm/release/shipit-notifier.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+"""shipit-agent.py [options]
+
+Listens for buildbot release messages from pulse and then sends them
+to the shipit REST API."""
+
+import uuid
+import site
+import datetime
+import pytz
+
+from optparse import OptionParser
+from ConfigParser import ConfigParser
+from release.info import getReleaseName
+from mozillapulse import consumers
+from mozillapulse import config as pconf
+from kickoff.api import Status
+from os import path
+from dateutil import parser
+
+site.addsitedir(path.join(path.dirname(__file__), "../../lib/python"))
+
+import logging as log
+
+NAME_KEYS = (u'product', u'version', u'build_number')
+PROPERTIES_KEYS = (u'platform', u'chunkTotal', u'chunkNum', u'event_group')
+
+
+def receive_message(config, data, message):
+    try:
+        if not data['payload']['build'].get(u'builderName').\
+                                        startswith('release-'):
+            return
+        if ' test ' in data['payload']['build'].get(u'builderName'):
+            return
+        if not data['payload']['build'].get('properties'):
+            log.error('TypeError: build properties not found - {}'.\
+                      format(data['payload']['build'].get('builderName')))
+            return
+        if data['payload'][u'results'] != 0:
+            return
+
+        log.info('msg received - {}'.format(data['payload']['build'].\
+                                     get(u'builderName')))
+
+        payload = {}
+        payload[u'sent'] = data['_meta'].get(u'sent')
+        payload[u'results'] = data['payload'].get(u'results')
+        payload[u'event_name'] = data['payload']['build'].get(u'builderName')
+
+        # Convert sent to UTC
+        timestamp = parser.parse(payload[u'sent']).astimezone(pytz.utc)
+        payload[u'sent'] = unicode(timestamp.strftime('%Y-%m-%d %H:%M:%S'))
+
+        for key in PROPERTIES_KEYS:
+            for prop in data['payload']['build'].get('properties'):
+                if prop[0] == key:
+                    try:
+                        payload[key] = prop[1]
+                    except IndexError as e:
+                        payload[key] = 'None'
+                        log.warning('{} not in build properties for {} - {}'.\
+                                    format(key, payload['event_name'], e))
+        payload[u'group'] = payload.pop(u'event_group')
+        if 'postrelease' in payload[u'event_name']:
+            payload[u'group'] = 'postrelease'
+
+        name = {}
+        for key in NAME_KEYS:
+            for prop in data['payload']['build'].get('properties'):
+                if prop[0] == key:
+                    try:
+                        name[key] = prop[1]
+                    except IndexError as e:
+                        name[key] = 'None'
+                        log.warning('{} not in build properties for {} - {}'.\
+                                    format(key, payload['event_name'], e))
+        name = getReleaseName(name.pop('product'), 
+                              name.pop('version'), 
+                              name.pop('build_number'))
+
+        log.info('adding new release event for {} with event_name {}'.\
+                 format(name, payload['event_name']))
+        status_api = Status((config.get('api', 'username'), 
+                             config.get('api', 'password')), 
+                            api_root=config.get('api', 'api_root'))
+        status_api.update(name, data=payload)
+        print payload
+    except Exception as e:
+        log.error('{} - {}'.format(e, data['payload']['build'].get('builderName')))
+    finally:
+        message.ack()
+
+
+def main():
+    parser = OptionParser()
+    parser.add_option("-c", "--config", dest="config",
+                      help="Configuration file")
+    options = parser.parse_args()[0]
+    config = ConfigParser()
+    try:
+        config.read(options.config)
+    except:
+        parser.error("Could not open configuration file")
+
+    def got_message(*args, **kwargs):
+        receive_message(config, *args, **kwargs)
+
+    if not options.config:
+        parser.error('Configuration file is required')
+
+    verbosity = {True: log.DEBUG, False: log.WARN}
+    log.basicConfig(
+        format='%(asctime)s %(message)s',
+        level=verbosity[config.getboolean('shipit-notifier', 'verbose')]
+    )
+
+    # Adjust unique_label when wanting to run shipit on multiple machines
+    unique_label = 'quickstart-{}'.format(uuid.uuid4())
+    
+    pulse = consumers.BuildConsumer(applabel=unique_label)
+    pulse.configure(topic='build.#.finished',
+                    durable=True, callback=got_message)
+
+    log.info('listening for pulse messages')
+    pulse.listen()
+
+if __name__ == "__main__":
+    main()
--- a/lib/python/kickoff/api.py
+++ b/lib/python/kickoff/api.py
@@ -46,24 +46,27 @@ class API(object):
                     config=self.config, timeout=self.timeout,
                     auth=self.auth)
                 self.csrf_token = res.headers['X-CSRF-Token']
             data['csrf_token'] = self.csrf_token
         log.debug('Request to %s' % url)
         log.debug('Data sent: %s' % data)
         try:
             return retry(self.session.request, sleeptime=5, max_sleeptime=15,
-                         retry_exceptions=(requests.HTTPError, requests.ConnectionError),
+                         retry_exceptions=(requests.HTTPError, 
+                                           requests.ConnectionError),
                          attempts=self.retries,
                          kwargs=dict(method=method, url=url, data=data,
                                      config=self.config, timeout=self.timeout,
                                      auth=self.auth, params=params)
-            )
+                        )
         except requests.HTTPError, e:
-            log.error('Caught HTTPError: %d %s' % (e.response.status_code, e.response.content), exc_info=True)
+            log.error('Caught HTTPError: %d %s' % 
+                     (e.response.status_code, e.response.content), 
+                     exc_info=True)
             raise
 
 
 class Releases(API):
     url_template = '/releases'
 
     def getReleases(self, ready=1, complete=0):
         resp = None
@@ -90,16 +93,25 @@ class Release(API):
             log.error('Caught error while getting release', exc_info=True)
             if resp:
                 log.error(resp.content)
                 log.error('Response code: %d' % resp.status_code)
             raise
 
     def update(self, name, **data):
         url_template_vars = {'name': name}
-        return self.request(method='POST', data=data, url_template_vars=url_template_vars).content
+        return self.request(method='POST', data=data, 
+                            url_template_vars=url_template_vars).content
 
 
 class ReleaseL10n(API):
     url_template = '/releases/%(name)s/l10n'
 
     def getL10n(self, name):
         return self.request(url_template_vars={'name': name}).content
+
+
+class Status(API):
+    url_template = '/releases/%(name)s/status'
+
+    def update(self, name, data):
+        return self.request(method='POST', data=data,
+                            url_template_vars={'name': name}).content
--- a/setup.py
+++ b/setup.py
@@ -20,16 +20,17 @@ setup(
     install_requires=[
         'sqlalchemy',
         'argparse',
         'twisted',
         'simplejson',
         'furl',
         'requests',
         'docopt',
+        'python-dateutil',
     ],
 
     entry_points={
         'console_scripts': [
             'slavealloc = slavealloc.scripts.main:main'
         ],
     },