# HG changeset patch # User cmlenz # Date 1120156032 0 # Node ID b92d7c7d70fd7eb108f6681240c7f3b65a0d1909 # Parent 234600bf0d49d95be12e4f3cc79d798f61675f79 Record build slave properties in database. diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -23,7 +23,7 @@ import time from trac.env import Environment -from bitten.model import Build, BuildConfig +from bitten.model import Build, BuildConfig, SlaveInfo from bitten.util import archive, beep, xmlio @@ -60,19 +60,16 @@ for config in BuildConfig.select(self.env): node = repos.get_node(config.path) for path, rev, chg in node.get_history(): - # Check whether the latest revision of that configuration has - # already been built + # Check whether the latest revision of that configuration + # has already been built builds = Build.select(self.env, config.name, rev) if not list(builds): logging.info('Enqueuing build of configuration "%s" as ' 'of revision [%s]', config.name, rev) build = Build(self.env) build.config = config.name - - chgset = repos.get_changeset(rev) build.rev = rev - build.rev_time = chgset.date - + build.rev_time = repos.get_changeset(rev).date build.insert() break finally: @@ -118,22 +115,26 @@ def handle_connect(self): self.master = self.session.listener assert self.master + self.env = self.master.env + assert self.env self.name = None + self.props = {} def handle_disconnect(self): if self.name is None: - # Slave didn't successfully register before disconnecting + # Slave didn't successfully register before disconnecting, so + # there's nothing to clean up return del self.master.slaves[self.name] - for build in Build.select(self.master.env, slave=self.name, + for build in Build.select(self.env, slave=self.name, status=Build.IN_PROGRESS): logging.info('Build [%s] of "%s" by %s cancelled', build.rev, build.config, self.name) build.slave = None build.status = Build.PENDING - build.time = None + build.started = 0 build.update() break logging.info('Unregistered slave "%s"', self.name) @@ -143,23 +144,22 @@ elem = xmlio.parse(msg.get_payload()) if elem.name == 'register': - platform, os, os_family, os_version = None, None, None, None + self.name = elem.attr['name'] for child in elem.children(): if child.name == 'platform': - platform = child.gettext() - processor = child.attr.get('processor') + self.props[SlaveInfo.MACHINE] = child.gettext() + self.props[SlaveInfo.PROCESSOR] = child.attr.get('processor') elif child.name == 'os': - os = child.gettext() - os_family = child.attr.get('family') - os_version = child.attr.get('version') + self.props[SlaveInfo.OS_NAME] = child.gettext() + self.props[SlaveInfo.OS_FAMILY] = child.attr.get('family') + self.props[SlaveInfo.OS_VERSION] = child.attr.get('version') self.name = elem.attr['name'] self.master.slaves[self.name] = self xml = xmlio.Element('ok') self.channel.send_rpy(msgno, beep.MIMEMessage(xml)) - logging.info('Registered slave "%s" (%s running %s %s [%s])', - self.name, platform, os, os_version, os_family) + logging.info('Registered slave "%s"', self.name) def send_initiation(self, build): logging.debug('Initiating build of "%s" on slave %s', build.config, @@ -211,7 +211,7 @@ if elem.name == 'started': self.steps = [] build.slave = self.name - build.time = int(time.time()) + build.started = int(time.time()) build.status = Build.IN_PROGRESS build.update() logging.info('Slave %s started build of "%s" as of [%s]', @@ -226,7 +226,7 @@ elif elem.name == 'abort': logging.info('Slave "%s" aborted build', self.name) build.slave = None - build.time = 0 + build.started = 0 build.status = Build.PENDING elif elem.name == 'error': build.status = Build.FAILURE @@ -234,7 +234,7 @@ if build.status != Build.PENDING: # Completed logging.info('Slave %s completed build of "%s" as of [%s]', self.name, build.config, build.rev) - build.duration = int(time.time()) - build.time + build.stopped = int(time.time()) if build.status is Build.IN_PROGRESS: # Find out whether the build failed or succeeded if [st for st in self.steps if st[1] == 'failure']: @@ -243,9 +243,16 @@ build.status = Build.SUCCESS else: # Aborted build.slave = None - build.time = 0 + build.started = 0 build.update() + # Insert slave info + slave_info = SlaveInfo(self.env) + slave_info.build = build.id + for name, value in self.props.items(): + slave_info[name] = value + slave_info.insert() + # TODO: should not block while reading the file; rather stream it using # asyncore push_with_producer() snapshot_path = self.master.get_snapshot(build, type, encoding) diff --git a/bitten/model.py b/bitten/model.py --- a/bitten/model.py +++ b/bitten/model.py @@ -18,7 +18,7 @@ # # Author: Christopher Lenz -from trac.db_default import Table, Column +from trac.db_default import Table, Column, Index schema_version = 1 @@ -119,10 +119,11 @@ class Build(object): """Representation of a build.""" - _table = Table('bitten_build', key=('config', 'rev', 'slave'))[ - Column('config'), Column('rev'), Column('slave'), - Column('time', type='int'), Column('duration', type='int'), - Column('status', size='1'), Column('rev_time', type='int') + _table = Table('bitten_build', key='id')[ + Column('id', auto_increment=True), Column('config'), Column('rev'), + Column('rev_time', type='int'), Column('slave'), + Column('started', type='int'), Column('stopped', type='int'), + Column('status', size='1'), Index(['config', 'rev', 'slave']) ] PENDING = 'P' @@ -130,35 +131,35 @@ SUCCESS = 'S' FAILURE = 'F' - def __init__(self, env, config=None, rev=None, slave=None, db=None): + def __init__(self, env, id=None, db=None): self.env = env - self.config = self.rev = self.slave = None - self.time = self.duration = self.rev_time = None - if config and rev and slave: - self._fetch(config, rev, slave, db) + if id is not None: + self._fetch(id, db) else: - self.time = self.duration = self.rev_time = 0 + self.id = self.config = self.rev = self.slave = None + self.started = self.stopped = self.rev_time = 0 self.status = self.PENDING - def _fetch(self, config, rev, slave, db=None): + def _fetch(self, id, db=None): if not db: db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute("SELECT time,duration,status,rev_time FROM bitten_build " - "WHERE config=%s AND rev=%s AND slave=%s", - (config, rev, slave)) + cursor.execute("SELECT config,rev,rev_time,slave,started,stopped," + "status FROM bitten_build WHERE id=%s", (id,)) row = cursor.fetchone() if not row: - raise Exception, "Build not found" - self.config = config - self.rev = rev - self.slave = slave - self.time = row[0] and int(row[0]) - self.duration = row[1] and int(row[1]) - self.status = row[2] - self.rev_time = int(row[3]) + raise Exception, "Build %s not found" % id + self.id = id + self.config = row[0] + self.rev = row[1] + self.rev_time = int(row[2]) + self.slave = row[3] + self.started = row[4] and int(row[4]) + self.stopped = row[5] and int(row[5]) + self.status = row[6] + exists = property(fget=lambda self: self.id is not None) completed = property(fget=lambda self: self.status != Build.IN_PROGRESS) successful = property(fget=lambda self: self.status == Build.SUCCESS) @@ -172,9 +173,7 @@ assert self.status == self.PENDING, 'Only pending builds can be deleted' cursor = db.cursor() - cursor.execute("DELETE FROM bitten_build WHERE config=%s AND rev=%s " - "AND slave=%s", (self.config, self.rev, - self.slave or '')) + cursor.execute("DELETE FROM bitten_build WHERE id=%s", (self.id,)) if handle_ta: db.commit() @@ -192,9 +191,12 @@ assert self.status == self.PENDING cursor = db.cursor() - cursor.execute("INSERT INTO bitten_build VALUES (%s,%s,%s,%s,%s,%s,%s)", - (self.config, self.rev, self.slave or '', self.time or 0, - self.duration or 0, self.status, self.rev_time)) + cursor.execute("INSERT INTO bitten_build (config,rev,rev_time,slave," + "started,stopped,status) " + "VALUES (%s,%s,%s,%s,%s,%s,%s)", + (self.config, self.rev, self.rev_time, self.slave or '', + self.started or 0, self.stopped or 0, self.status)) + self.id = db.get_last_id('bitten_build') if handle_ta: db.commit() @@ -212,10 +214,10 @@ assert self.status == self.PENDING cursor = db.cursor() - cursor.execute("UPDATE bitten_build SET slave=%s,time=%s,duration=%s," - "status=%s WHERE config=%s AND rev=%s", - (self.slave or '', self.time or 0, self.duration or 0, - self.status, self.config, self.rev)) + cursor.execute("UPDATE bitten_build SET slave=%s,started=%s," + "stopped=%s,status=%s WHERE id=%s", + (self.slave or '', self.started or 0, + self.stopped or 0, self.status, self.id)) if handle_ta: db.commit() @@ -239,17 +241,78 @@ where = "" cursor = db.cursor() - cursor.execute("SELECT config,rev,slave,time,duration,status,rev_time " - "FROM bitten_build %s ORDER BY config,rev_time DESC," - "slave" % where, [wc[1] for wc in where_clauses]) - for config, rev, slave, time, duration, status, rev_time in cursor: + cursor.execute("SELECT id,config,rev,slave,started,stopped,status," + "rev_time FROM bitten_build %s " + "ORDER BY config,rev_time DESC,slave" + % where, [wc[1] for wc in where_clauses]) + for id, config, rev, slave, started, stopped, status, rev_time \ + in cursor: build = Build(env) + build.id = id build.config = config build.rev = rev build.slave = slave - build.time = time and int(time) or 0 - build.duration = duration and int(duration) or 0 + build.started = started and int(started) or 0 + build.stopped = stopped and int(stopped) or 0 build.status = status build.rev_time = int(rev_time) yield build select = classmethod(select) + + +class SlaveInfo(object): + _table = Table('bitten_slave', key=('build', 'propname'))[ + Column('build', type='int'), Column('propname'), Column('propvalue') + ] + + # Standard properties + IP_ADDRESS = 'ipnr' + MAINTAINER = 'owner' + OS_NAME = 'os' + OS_FAMILY = 'family' + OS_VERSION = 'version' + MACHINE = 'machine' + PROCESSOR = 'processor' + + def __init__(self, env, build=None, db=None): + self.env = env + self.properties = {} + if build: + self._fetch(build, db) + else: + self.build = None + + def _fetch(self, build, db=None): + if not db: + db = self.env.get_db_cnx() + + cursor = db.cursor() + cursor.execute("SELECT propname,propvalue FROM bitten_slave " + "WHERE build=%s", (build,)) + for propname, propvalue in cursor: + self.properties[propname] = propvalue + if not self.properties: + raise Exception, "Slave info for '%s' not found" % name + self.build = build + + def __getitem__(self, name): + return self.properties[name] + + def __setitem__(self, name, value): + self.properties[name] = value + + def insert(self, db=None): + if not db: + db = self.env.get_db_cnx() + handle_ta = True + else: + handle_ta = False + + assert self.build + + cursor = db.cursor() + cursor.executemany("INSERT INTO bitten_slave VALUES (%s,%s,%s)", + [(self.build, name, value) for name, value + in self.properties.items()]) + if handle_ta: + db.commit() diff --git a/bitten/slave.py b/bitten/slave.py --- a/bitten/slave.py +++ b/bitten/slave.py @@ -131,7 +131,6 @@ def execute_build(self, msgno, basedir, recipe_path): logging.info('Building in directory %s using recipe %s', basedir, recipe_path) - try: recipe = Recipe(recipe_path, basedir) diff --git a/bitten/tests/model.py b/bitten/tests/model.py --- a/bitten/tests/model.py +++ b/bitten/tests/model.py @@ -21,7 +21,7 @@ import unittest from trac.test import EnvironmentStub -from bitten.model import Build, BuildConfig +from bitten.model import Build, BuildConfig, SlaveInfo class BuildConfigTestCase(unittest.TestCase): @@ -66,9 +66,10 @@ def test_new_build(self): build = Build(self.env) + self.assertEqual(None, build.id) self.assertEqual(Build.PENDING, build.status) - self.assertEqual(0, build.time) - self.assertEqual(0, build.duration) + self.assertEqual(0, build.stopped) + self.assertEqual(0, build.started) def test_insert_build(self): build = Build(self.env) @@ -79,8 +80,8 @@ db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute("SELECT config,rev,slave,time,duration,status " - "FROM bitten_build") + cursor.execute("SELECT config,rev,slave,started,stopped,status " + "FROM bitten_build WHERE id=%s" % build.id) self.assertEqual(('test', '42', '', 0, 0, 'P'), cursor.fetchone()) def test_insert_build_no_config_or_rev_or_rev_time(self): @@ -125,10 +126,48 @@ self.assertRaises(AssertionError, build.insert) +class SlaveInfoTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute(db.to_sql(SlaveInfo._table)) + db.commit() + + def test_insert_slave_info(self): + slave_info = SlaveInfo(self.env) + slave_info.build = 42 + slave_info[SlaveInfo.IP_ADDRESS] = '127.0.0.1' + slave_info[SlaveInfo.MAINTAINER] = 'joe@example.org' + slave_info.insert() + + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.execute("SELECT propname,propvalue FROM bitten_slave") + expected = {SlaveInfo.IP_ADDRESS: '127.0.0.1', + SlaveInfo.MAINTAINER: 'joe@example.org'} + for propname, propvalue in cursor: + self.assertEqual(expected[propname], propvalue) + + def test_fetch_slave_info(self): + db = self.env.get_db_cnx() + cursor = db.cursor() + cursor.executemany("INSERT INTO bitten_slave VALUES (42,%s,%s)", + [(SlaveInfo.IP_ADDRESS, '127.0.0.1'), + (SlaveInfo.MAINTAINER, 'joe@example.org')]) + + slave_info = SlaveInfo(self.env, 42) + self.assertEquals(42, slave_info.build) + self.assertEquals('127.0.0.1', slave_info[SlaveInfo.IP_ADDRESS]) + self.assertEquals('joe@example.org', slave_info[SlaveInfo.MAINTAINER]) + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(BuildConfigTestCase, 'test')) suite.addTest(unittest.makeSuite(BuildTestCase, 'test')) + suite.addTest(unittest.makeSuite(SlaveInfoTestCase, 'test')) return suite if __name__ == '__main__': diff --git a/bitten/trac_ext/main.py b/bitten/trac_ext/main.py --- a/bitten/trac_ext/main.py +++ b/bitten/trac_ext/main.py @@ -23,7 +23,7 @@ from trac.core import * from trac.env import IEnvironmentSetupParticipant from trac.perm import IPermissionRequestor -from bitten.model import Build, BuildConfig, schema_version +from bitten.model import Build, BuildConfig, SlaveInfo, schema_version from bitten.trac_ext import web_ui class BuildSystem(Component): @@ -36,7 +36,7 @@ # Create the required tables db = self.env.get_db_cnx() cursor = db.cursor() - for table in [Build._table, BuildConfig._table]: + for table in [Build._table, BuildConfig._table, SlaveInfo._table]: cursor.execute(db.to_sql(table)) tarballs_dir = os.path.join(self.env.path, 'snapshots') diff --git a/bitten/trac_ext/web_ui.py b/bitten/trac_ext/web_ui.py --- a/bitten/trac_ext/web_ui.py +++ b/bitten/trac_ext/web_ui.py @@ -27,7 +27,7 @@ from trac.web.chrome import INavigationContributor from trac.web.main import IRequestHandler from trac.wiki import wiki_to_html -from bitten.model import Build, BuildConfig +from bitten.model import Build, BuildConfig, SlaveInfo class BuildModule(Component): @@ -93,34 +93,23 @@ var:build.config.browser_href ?>">
-

Builds

(None)

+

Triggered by: Changeset [] of

+

Built by: ( on )

+

Completed: ( ago)
Took:

@@ -128,7 +117,7 @@ """ _status_label = {Build.IN_PROGRESS: 'in progress', - Build.SUCCESS: 'success', + Build.SUCCESS: 'completed', Build.FAILURE: 'failed'} # INavigationContributor methods @@ -146,7 +135,7 @@ # IRequestHandler methods def match_request(self, req): - match = re.match(r'/build(?:/([\w.-]+))?(?:/([\w+.-]))?', req.path_info) + match = re.match(r'/build(?:/([\w.-]+))?(?:/([\w.-]+))?', req.path_info) if match: if match.group(1): req.args['config'] = match.group(1) @@ -192,20 +181,20 @@ if 'build' in filters: db = self.env.get_db_cnx() cursor = db.cursor() - cursor.execute("SELECT name,label,rev,slave,time,status " + cursor.execute("SELECT id,config,label,rev,slave,stopped,status " "FROM bitten_build " " INNER JOIN bitten_config ON (name=config) " - "WHERE time>=%s AND time<=%s " - "AND status IN (%s, %s) ORDER BY time", + "WHERE stopped>=%s AND stopped<=%s " + "AND status IN (%s, %s) ORDER BY stopped", (start, stop, Build.SUCCESS, Build.FAILURE)) event_kinds = {Build.SUCCESS: 'successbuild', Build.FAILURE: 'failedbuild'} - for name, label, rev, slave, time, status in cursor: - title = '%s [%s] built by %s' \ - % (escape(label), escape(rev), escape(slave)) - href = self.env.href.build(name) - message = self._status_label[status] - yield event_kinds[status], href, title, time, None, message + for id, config, label, rev, slave, stopped, status in cursor: + title = 'Build %s by %s %s' \ + % (escape(rev), escape(label), escape(id), + escape(slave), self._status_label[status]) + href = self.env.href.build(config, id) + yield event_kinds[status], href, title, stopped, None, '' # Internal methods @@ -271,35 +260,6 @@ 'browser_href': self.env.href.browser(config.path) } - builds = Build.select(self.env, config=config.name) - curr_rev = None - slave_idx = 0 - for idx, build in enumerate(builds): - if build.rev != curr_rev: - slave_idx = 0 - curr_rev = build.rev - req.hdf['build.config.builds.%d' % idx] = { - 'rev': build.rev, - 'href': self.env.href.changeset(build.rev) - } - if not build.slave: - continue - prefix = 'build.config.builds.%d.slaves.%d' % (idx, slave_idx) - req.hdf[prefix] = {'name': build.slave, - 'status': self._status_label[build.status]} - if build.time: - started = build.time - req.hdf[prefix + '.started'] = strftime('%x %X', - localtime(started)) - req.hdf[prefix + '.started_delta'] = pretty_timedelta(started) - if build.duration: - stopped = build.time + build.duration - req.hdf[prefix + '.duration'] = pretty_timedelta(stopped, - build.time) - req.hdf[prefix + '.stopped'] = strftime('%x %X', - localtime(stopped)) - req.hdf[prefix + '.stopped_delta'] = pretty_timedelta(stopped) - req.hdf['build.mode'] = 'view_config' req.hdf['build.can_modify'] = req.perm.has_permission('BUILD_MODIFY') @@ -320,4 +280,40 @@ req.hdf['build.mode'] = 'edit_config' def _render_build(self, req, config_name, build_id): - raise NotImplementedError, 'Not implemented yet' + build = Build(self.env, build_id) + assert build.exists + status2title = {Build.SUCCESS: 'Success', Build.FAILURE: 'Failure'} + req.hdf['title'] = 'Build %s - %s' % (build_id, + status2title[build.status]) + req.hdf['build'] = self._build_to_hdf(build) + req.hdf['build.mode'] = 'view_build' + + config = BuildConfig(self.env, build.config) + req.hdf['build.config'] = { + 'name': config.label, + 'href': self.env.href.build(config.name) + } + + slave_info = SlaveInfo(self.env, build.id) + req.hdf['build.slave'] = { + 'name': build.slave, + 'ip_address': slave_info.properties.get(SlaveInfo.IP_ADDRESS), + 'os': slave_info.properties.get(SlaveInfo.OS_NAME), + 'os.family': slave_info.properties.get(SlaveInfo.OS_FAMILY), + 'os.version': slave_info.properties.get(SlaveInfo.OS_VERSION), + 'machine': slave_info.properties.get(SlaveInfo.MACHINE), + 'processor': slave_info.properties.get(SlaveInfo.PROCESSOR) + } + + def _build_to_hdf(self, build): + hdf = {'name': build.slave, 'status': self._status_label[build.status], + 'rev': build.rev, + 'chgset_href': self.env.href.changeset(build.rev)} + if build.started: + hdf['started'] = strftime('%x %X', localtime(build.started)) + hdf['started_delta'] = pretty_timedelta(build.started) + if build.stopped: + hdf['stopped'] = strftime('%x %X', localtime(build.stopped)) + hdf['stopped_delta'] = pretty_timedelta(build.stopped) + hdf['duration'] = pretty_timedelta(build.stopped, build.started) + return hdf