From 0ab979330581ebc10382339d74f2d47b79c15a1c Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Fri, 15 Jul 2016 23:20:23 -0400 Subject: [PATCH] Some more state transitions. --- rida.py | 48 ++++++++----------- rida/database.py | 53 ++++++++++++++++++++- rida/scheduler/handlers/components.py | 66 +++++++++++++++++++++++++++ rida/scheduler/handlers/modules.py | 33 ++++---------- rida/scheduler/handlers/repos.py | 63 +++++++++++++++++++++++++ rida/scheduler/main.py | 10 ++-- 6 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 rida/scheduler/handlers/components.py create mode 100644 rida/scheduler/handlers/repos.py diff --git a/rida.py b/rida.py index 1c2eb01e..cb6c84e5 100755 --- a/rida.py +++ b/rida.py @@ -1,7 +1,5 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- - - # Copyright (c) 2016 Red Hat, Inc. # # Permission is hereby granted, free of charge, to any person obtaining a copy @@ -30,6 +28,7 @@ This is the implementation of the orchestrator's public RESTful API. """ from flask import Flask, request +import flask import json import logging import modulemd @@ -38,9 +37,9 @@ import rida.auth import rida.config import rida.database import rida.logger -import rida.messaging import rida.scm import ssl +import shutil import tempfile app = Flask(__name__) @@ -50,6 +49,8 @@ app.config.from_envvar("RIDA_SETTINGS", silent=True) conf = rida.config.from_file("rida.conf") rida.logger.init_logging(conf) +log = logging.getLogger(__name__) + db = rida.database.Database(conf) @app.route("/rida/module-builds/", methods=["POST"]) @@ -77,13 +78,12 @@ def submit_build(): return "The submitted scmurl isn't allowed", 403 yaml = str() try: + td = tempfile.mkdtemp() scm = rida.scm.SCM(url, conf.scmurls) - td = tempfile.TemporaryDirectory() - cod = scm.checkout(td.name) + cod = scm.checkout(td) cofn = os.path.join(cod, (scm.name + ".yaml")) with open(cofn, "r") as mmdfile: yaml = mmdfile.read() - td.cleanup() except Exception as e: if "is not in the list of allowed SCMs" in str(e): rc = 403 @@ -92,6 +92,8 @@ def submit_build(): else: rc = 500 return str(e), rc + finally: + shutil.rmtree(td) mmd = modulemd.ModuleMetadata() try: mmd.loads(yaml) @@ -100,25 +102,19 @@ def submit_build(): if db.session.query(rida.database.ModuleBuild).filter_by(name=mmd.name, version=mmd.version, release=mmd.release).first(): return "Module already exists", 409 - module = rida.database.ModuleBuild( + module = rida.database.ModuleBuild.create( + db.session, + conf, name=mmd.name, version=mmd.version, release=mmd.release, - state="init", modulemd=yaml, ) - db.session.add(module) - db.session.commit() - rida.messaging.publish( - modname='rida', - topic='module.state.change', - msg=module.json(), # Note the state is "init" here... - backend=conf.messaging, - ) def failure(message, code): - # TODO, we should make some note of why it failed in a log... - module.transition(rida.database.BUILD_STATES["failed"]) + # TODO, we should make some note of why it failed in the db.. + log.exception(message) + module.transition(conf, rida.database.BUILD_STATES["failed"]) db.session.add(module) db.session.commit() return message, code @@ -142,26 +138,18 @@ def submit_build(): build = rida.database.ComponentBuild(module_id=module.id, package=pkgname, format="rpms") db.session.add(build) module.modulemd = mmd.dumps() - module.transition(rida.database.BUILD_STATES["wait"]) + module.transition(conf, rida.database.BUILD_STATES["wait"]) db.session.add(module) db.session.commit() - # Publish to whatever bus we're configured to connect to. - # This should notify ridad to start doing the work we just scheduled. - rida.messaging.publish( - modname='rida', - topic='module.state.change', - msg=module.json(), # Note the state is "wait" here... - backend=conf.messaging, - ) logging.info("%s submitted build of %s-%s-%s", username, mmd.name, mmd.version, mmd.release) - return json.dumps(module.json()), 201 + return flask.jsonify(module.json()), 201 @app.route("/rida/module-builds/", methods=["GET"]) def query_builds(): """Lists all tracked module builds.""" - return json.dumps([{"id": x.id, "state": x.state} + return flask.jsonify([{"id": x.id, "state": x.state} for x in db.session.query(rida.database.ModuleBuild).all()]), 200 @@ -175,7 +163,7 @@ def query_build(id): for build in db.session.query(rida.database.ComponentBuild).filter_by(module_id=id).all(): tasks[build.format + "/" + build.package] = \ str(build.task) + "/" + build.state - return json.dumps({ + return flask.jsonify({ "id": module.id, "state": module.state, "tasks": tasks diff --git a/rida/database.py b/rida/database.py index ff63e3ec..772bb50a 100644 --- a/rida/database.py +++ b/rida/database.py @@ -38,6 +38,8 @@ from sqlalchemy.orm import ( ) from sqlalchemy.ext.declarative import declarative_base +import rida.messaging + import logging log = logging.getLogger(__name__) @@ -129,6 +131,7 @@ class ModuleBuild(Base): release = Column(String, nullable=False) state = Column(Integer, nullable=False) modulemd = Column(String, nullable=False) + koji_tag = Column(String) # This gets set after 'wait' module = relationship('Module', backref='module_builds', lazy=False) @@ -146,11 +149,57 @@ class ModuleBuild(Base): raise ValueError("%r is not a module message." % msg['topic']) return session.query(cls).filter(cls.id==msg['msg']['id']).one() - def transition(self, state): + @classmethod + def create(cls, session, conf, name, version, release, modulemd): + module = cls( + name=name, + version=version, + release=release, + state="init", + modulemd=modulemd, + ) + session.add(module) + session.commit() + rida.messaging.publish( + modname='rida', + topic='module.state.change', + msg=module.json(), # Note the state is "init" here... + backend=conf.messaging, + ) + return module + + def transition(self, conf, state): """ Record that a build has transitioned state. """ old_state = self.state self.state = state - log.debug("%r, state %r->%r" % (old_state, self.state)) + log.debug("%r, state %r->%r" % (self, old_state, self.state)) + rida.messaging.publish( + modname='rida', + topic='module.state.change', + msg=self.json(), # Note the state is "init" here... + backend=conf.messaging, + ) + + @classmethod + def get_active_by_koji_tag(cls, session, koji_tag): + """ Find the ModuleBuilds in our database that should be in-flight... + ... for a given koji tag. + + There should be at most one. + """ + query = session.query(rida.database.ModuleBuild)\ + .filter_by(koji_tag=koji_tag)\ + .filter_by(state="build") + + count = query.count() + if count > 1: + raise RuntimeError("%r module builds in flight for %r" % (count, koji_tag)) + elif count == 0: + # No builds in flight scheduled by us. Just ignore this. + return None + + # Otherwise, there is exactly one module build - it must be ours. + return query.one() def json(self): return { diff --git a/rida/scheduler/handlers/components.py b/rida/scheduler/handlers/components.py new file mode 100644 index 00000000..14509361 --- /dev/null +++ b/rida/scheduler/handlers/components.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Ralph Bean + +""" Handlers for koji component build events on the message bus. """ + +import rida.builder +import rida.database +import rida.pdc +import logging +import koji + +log = logging.getLogger(__name__) + + +def complete(config, session, msg): + """ Called whenever a koji build completes. """ + + # First, find our ModuleBuild associated with this repo, if any. + component_build = rida.database.ComponentBuild.from_fedmsg(session, msg) + if not component_build: + template = "We have no record of {name}-{version}-{release}" + log.debug(template.format(**msg['msg'])) + return + + # Mark the state in the db. + component_build.state = koji.BUILD_STATES['COMPLETE'] + session.commit() + + # Find all of the sibling builds of this particular build. + parent = component_build.module_build + siblings = parent.component_builds + + # Are any of them still executing? + if any([c.state == koji.BUILD_STATES['BUILDING'] for c in siblings]): + # Then they're not all done yet... continue to wait + return + + # Otherwise, check to see if any failed. + if any([c.state != koji.BUILD_STATES['COMPLETE'] for c in siblings]): + # They didn't all succeed.. so mark this module build as a failure. + parent.transition(config, rida.BUILD_STATES['failed']) + session.commit() + return + + # Otherwise.. if all of the builds succeeded, then mark the module as good. + parent.transition(config, rida.BUILD_STATES['done']) diff --git a/rida/scheduler/handlers/modules.py b/rida/scheduler/handlers/modules.py index f6ba125e..5c902c52 100644 --- a/rida/scheduler/handlers/modules.py +++ b/rida/scheduler/handlers/modules.py @@ -47,7 +47,13 @@ def wait(config, session, msg): module_info = build.json() log.debug("Received module_info=%s from pdc" % module_info) tag = rida.pdc.get_module_tag(pdc_session, module_info) - log.info("Found tag=%s for module %r" % (tag, build)) + log.debug("Found tag=%s for module %r" % (tag, build)) + + # Hang on to this information for later. We need to know which build is + # associated with which koji tag, so that when their repos are regenerated + # in koji we can figure out which for which module build that event is + # relevant. + build.tag = tag dependencies = rida.pdc.get_module_dependencies(pdc_session, module_info) builder = rida.builder.KojiModuleBuilder(build.name, config) @@ -57,30 +63,7 @@ def wait(config, session, msg): # TODO submit build from srpm to koji # TODO: buildroot.add_artifact(build_with_dist_tags) # TODO: buildroot.ready(artifact=$artifact) - build.transition(state="build") # Wait for the buildroot to be ready. + build.transition(conf, state="build") # Wait for the buildroot to be ready. session.commit() -def build(config, session, msg): - """ Called whenever a module enters the "build" state. - - We usually transition to this state once the buildroot is ready. - - All we do here is kick off builds of all our components. - """ - module_build = rida.database.ModuleBuild.from_fedmsg(session, msg) - builder = rida.builder.KojiModuleBuilder(build.name, config) - builder.buildroot_resume() - - for component_build in module_build.component_builds: - scmurl = "{dist_git}/rpms/{package}?#{gitref}".format( - dist_git=config.dist_git_url, - package=component_build.package, - gitref=component_build.gitref, # This is the update stream - ) - artifact_name = 'TODO' - component_build.task = builder.build(artifact_name, scmurl) - component_build.state = koji.BUILD_STATES['BUILDING'] - - build.transition(state="build") # Now wait for all of those to finish. - session.commit() diff --git a/rida/scheduler/handlers/repos.py b/rida/scheduler/handlers/repos.py new file mode 100644 index 00000000..74ce58c2 --- /dev/null +++ b/rida/scheduler/handlers/repos.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Ralph Bean + +""" Handlers for repo change events on the message bus. """ + +import rida.builder +import rida.database +import rida.pdc +import logging +import koji + +log = logging.getLogger(__name__) + + +def done(config, session, msg): + """ Called whenever koji rebuilds a repo, any repo. """ + + # First, find our ModuleBuild associated with this repo, if any. + tag = msg['msg']['tag'] + module_build = rida.database.ModuleBuild.get_active_by_koji_tag( + session, koji_tag=tag) + if not module_build: + log.debug("No module build found associated with koji tag %r" % tag) + return + + unbuilt_components = ( + component_build for component_build in module_build.component_builds + if component_build.state is None + ) + + builder = rida.builder.KojiModuleBuilder(module_build.name, config) + builder.buildroot_resume() + + for component_build in unbuilt_components: + scmurl = "{dist_git}/rpms/{package}?#{gitref}".format( + dist_git=config.dist_git_url, + package=component_build.package, + gitref=component_build.gitref, # This is the update stream + ) + artifact_name = 'TODO' + component_build.state = koji.BUILD_STATES['BUILDING'] + component_build.task = builder.build(artifact_name, scmurl) + session.commit() diff --git a/rida/scheduler/main.py b/rida/scheduler/main.py index 5ff770fa..ea0cf1eb 100644 --- a/rida/scheduler/main.py +++ b/rida/scheduler/main.py @@ -76,8 +76,10 @@ class Messaging(threading.Thread): koji.BUILD_STATES["BUILDING"]: lambda x: x } on_module_change = { - rida.BUILD_STATES["init"]: rida.scheduler.handlers.modules.init, + rida.BUILD_STATES["wait"]: rida.scheduler.handlers.modules.wait, } + # Only one kind of repo change event... + on_repo_change = rida.scheduler.handlers.repos.done, def sanity_check(self): """ On startup, make sure our implementation is sane. """ @@ -107,8 +109,10 @@ class Messaging(threading.Thread): log.debug(msg) # Choose a handler for this message - if '.buildsys.build.state.change' in msg['topic']: - handler = self.on_build_change[msg['msg']['init']] + if '.buildsys.repo.done' in msg['topic']: + handler = self.on_repo_change + elif '.buildsys.build.state.change' in msg['topic']: + handler = self.on_build_change[msg['msg']['new']] elif '.rida.module.state.change' in msg['topic']: handler = self.on_module_change[module_build_state_from_msg(msg)] else: