From 9b06951e6693a9541dad03b61ab87b32da9b8f97 Mon Sep 17 00:00:00 2001
From: Jan Kaluza <jkaluza@redhat.com>
Date: Nov 14 2017 07:08:57 +0000
Subject: Allow manual trigger of container images rebuild.


---

diff --git a/conf/config.py b/conf/config.py
index e950f36..2e48831 100644
--- a/conf/config.py
+++ b/conf/config.py
@@ -21,7 +21,7 @@ class BaseConfiguration(object):
     HOST = '0.0.0.0'
     PORT = 5001
 
-    SERVER_NAME = 'localhost'
+    SERVER_NAME = 'localhost:5001'
 
     DEBUG = False
     # Global network-related values, in seconds
@@ -46,6 +46,7 @@ class BaseConfiguration(object):
 
     # Parsers defined for parse specific messages
     PARSERS = [
+        'freshmaker.parsers.internal:FreshmakerManualRebuildParser',
         'freshmaker.parsers.bodhi:BodhiUpdateCompleteStableParser',
         'freshmaker.parsers.git:GitReceiveParser',
         'freshmaker.parsers.koji:KojiTaskStateChangeParser',
@@ -54,6 +55,7 @@ class BaseConfiguration(object):
 
     # List of enabled composing handlers.
     HANDLERS = [
+        "freshmaker.handlers.internal:FreshmakerManualRebuildHandler",
         "freshmaker.handlers.bodhi:BodhiUpdateCompleteStableHandler",
         "freshmaker.handlers.git:GitDockerfileChangeHandler",
         "freshmaker.handlers.git:GitModuleMetadataChangeHandler",
@@ -243,6 +245,7 @@ class TestConfiguration(BaseConfiguration):
         os.path.join(dbdir, 'tests', 'test_freshmaker.db'))
 
     MESSAGING = 'in_memory'
+    MESSAGING_SENDER = 'in_memory'
     PDC_URL = 'http://pdc.fedoraproject.org/rest_api/v1'
 
     # Global network-related values, in seconds
diff --git a/conf/configrh.py b/conf/configrh.py
index 82980ba..015a35d 100644
--- a/conf/configrh.py
+++ b/conf/configrh.py
@@ -56,6 +56,11 @@ class BaseConfiguration(config.BaseConfiguration):
     PULP_USERNAME = ''
     PULP_PASSWORD = ''
 
+    AUTH_BACKEND = 'kerberos'
+    # Replace with real ldap server URL
+    AUTH_LDAP_SERVER = ''
+    AUTH_LDAP_GROUP_BASE = 'ou=groups,dc=redhat,dc=com'
+
 
 class DevConfiguration(BaseConfiguration):
     DEBUG = True
diff --git a/freshmaker/__init__.py b/freshmaker/__init__.py
index 80882e6..d56746a 100644
--- a/freshmaker/__init__.py
+++ b/freshmaker/__init__.py
@@ -43,10 +43,10 @@ db = SQLAlchemy(app)
 init_logging(conf)
 log = getLogger(__name__)
 
-from freshmaker import views  # noqa
-
 login_manager = LoginManager()
 login_manager.init_app(app)
 
 from freshmaker.auth import init_auth  # noqa
 init_auth(login_manager, conf.auth_backend)
+
+from freshmaker import views  # noqa
diff --git a/freshmaker/auth.py b/freshmaker/auth.py
index 3f6e44c..d3fda62 100644
--- a/freshmaker/auth.py
+++ b/freshmaker/auth.py
@@ -107,7 +107,7 @@ def load_openidc_user(request):
 
     token = request.environ.get('OIDC_access_token')
     if not token:
-        raise Unauthorized('Missing token passed to ODCS.')
+        raise Unauthorized('Missing token passed to Freshmaker.')
 
     scope = request.environ.get('OIDC_CLAIM_scope')
     if not scope:
@@ -184,7 +184,7 @@ def init_auth(login_manager, backend):
     if backend == 'noauth':
         # Do not enable any authentication backend working with frontend
         # authentication module in Apache.
-        log.warn("Authorization is disabled in ODCS configuration.")
+        log.warn("Authorization is disabled in Freshmaker configuration.")
         return
     if backend == 'kerberos':
         _validate_kerberos_config()
diff --git a/freshmaker/config.py b/freshmaker/config.py
index 5403672..0c93b85 100644
--- a/freshmaker/config.py
+++ b/freshmaker/config.py
@@ -127,6 +127,10 @@ class Config(object):
             'type': str,
             'default': 'fedmsg',
             'desc': 'The messaging system to use.'},
+        'messaging_sender': {
+            'type': str,
+            'default': 'fedmsg',
+            'desc': 'The messaging system to use for sending msgs.'},
         'messaging_topic_prefix': {
             'type': list,
             'default': ['org.fedoraproject.prod'],
@@ -365,3 +369,9 @@ class Config(object):
         if s not in ("fedmsg", "amq", "in_memory", "rhmsg"):
             raise ValueError("Unsupported messaging system.")
         self._messaging = s
+
+    def _setifok_messaging_sender(self, s):
+        s = str(s)
+        if s not in ("fedmsg", "amq", "in_memory", "rhmsg"):
+            raise ValueError("Unsupported messaging system.")
+        self._messaging_sender = s
diff --git a/freshmaker/errata.py b/freshmaker/errata.py
index 940b002..1f14fd4 100644
--- a/freshmaker/errata.py
+++ b/freshmaker/errata.py
@@ -25,7 +25,9 @@ import requests
 import dogpile.cache
 from requests_kerberos import HTTPKerberosAuth
 
-from freshmaker.events import BrewSignRPMEvent, ErrataAdvisoryStateChangedEvent
+from freshmaker.events import (
+    BrewSignRPMEvent, ErrataAdvisoryStateChangedEvent,
+    FreshmakerManualRebuildEvent)
 from freshmaker import conf, log
 
 
@@ -61,14 +63,17 @@ class Errata(object):
     product_region = dogpile.cache.make_region().configure(
         conf.dogpile_cache_backend, expiration_time=24 * 3600)
 
-    def __init__(self, server_url):
+    def __init__(self, server_url=None):
         """
         Initializes the Errata instance.
 
         :param str server_url: Base URL of Errata server.
         """
         self._rest_api_ver = 'api/v1'
-        self.server_url = server_url.rstrip('/')
+        if server_url is not None:
+            self.server_url = server_url.rstrip('/')
+        else:
+            self.server_url = conf.errata_tool_server_url.rstrip('/')
 
     def _errata_rest_get(self, endpoint):
         """Request REST-style API
@@ -92,6 +97,9 @@ class Errata(object):
         r.raise_for_status()
         return r.json()
 
+    def get_advisory(self, errata_id):
+        return self._errata_http_get('advisory/{0}.json'.format(errata_id))
+
     @region.cache_on_arguments()
     def _advisories_from_nvr(self, nvr):
         """
@@ -127,9 +135,9 @@ class Errata(object):
         """
         if isinstance(event, BrewSignRPMEvent):
             return self._advisories_from_nvr(event.nvr)
-        elif isinstance(event, ErrataAdvisoryStateChangedEvent):
-            data = self._errata_http_get(
-                "advisory/%s.json" % str(event.errata_id))
+        elif (isinstance(event, ErrataAdvisoryStateChangedEvent) or
+              isinstance(event, FreshmakerManualRebuildEvent)):
+            data = self.get_advisory(event.errata_id)
             advisory = ErrataAdvisory(
                 data["id"], data["advisory_name"], data["status"],
                 data["security_impact"])
diff --git a/freshmaker/events.py b/freshmaker/events.py
index ffcd100..ddbc0aa 100644
--- a/freshmaker/events.py
+++ b/freshmaker/events.py
@@ -35,12 +35,15 @@ class BaseEvent(object):
 
     _parsers = {}
 
-    def __init__(self, msg_id):
+    def __init__(self, msg_id, manual=False):
         """
         A base class to abstract events from different fedmsg messages.
         :param msg_id: the id of the msg (e.g. 2016-SomeGUID)
+        :param manual: True if the event was trigerred manually by Freshmaker
+            REST API.
         """
         self.msg_id = msg_id
+        self.manual = manual
 
         # Moksha calls `consumer.validate` on messages that it receives, and
         # even though we have validation turned off in the config there's still
@@ -310,5 +313,11 @@ class ODCSComposeStateChangeEvent(BaseEvent):
     """Represent a compose' state change event from ODCS"""
 
     def __init__(self, msg_id, compose):
-        self.msg_id = msg_id
+        super(ODCSComposeStateChangeEvent, self).__init__(msg_id)
         self.compose = compose
+
+
+class FreshmakerManualRebuildEvent(BaseEvent):
+    def __init__(self, msg_id, errata_id=None):
+        super(FreshmakerManualRebuildEvent, self).__init__(msg_id)
+        self.errata_id = errata_id
diff --git a/freshmaker/handlers/brew/sign_rpm.py b/freshmaker/handlers/brew/sign_rpm.py
index 8b192a7..9fc7f7a 100644
--- a/freshmaker/handlers/brew/sign_rpm.py
+++ b/freshmaker/handlers/brew/sign_rpm.py
@@ -21,7 +21,6 @@
 #
 # Written by Chenxiong Qi <cqi@redhat.com>
 
-from freshmaker import conf
 from freshmaker import log
 from freshmaker import db
 from freshmaker.events import BrewSignRPMEvent, ErrataAdvisoryRPMsSignedEvent
@@ -68,7 +67,7 @@ class BrewSignRPMHandler(BaseHandler):
 
         # When get a signed RPM, first step is to find out advisories
         # containing that RPM and ensure all builds are signed.
-        errata = Errata(conf.errata_tool_server_url)
+        errata = Errata()
         advisories = errata.advisories_from_event(event)
 
         # Filter out advisories which are not allowed by configuration.
diff --git a/freshmaker/handlers/errata/errata_advisory_rpms_signed.py b/freshmaker/handlers/errata/errata_advisory_rpms_signed.py
index d536a47..f755d23 100644
--- a/freshmaker/handlers/errata/errata_advisory_rpms_signed.py
+++ b/freshmaker/handlers/errata/errata_advisory_rpms_signed.py
@@ -65,18 +65,21 @@ class ErrataAdvisoryRPMsSignedHandler(BaseHandler):
         advisory.
         """
 
+        self.event = event
+
         # Check if we are allowed to build this advisory.
-        if not self.allow_build(
+        if not event.manual and not self.allow_build(
                 ArtifactType.IMAGE, advisory_name=event.errata_name,
                 advisory_security_impact=event.security_impact):
-            log.info("Errata advisory %s not allowed to trigger rebuilds.",
-                     event.errata_name)
+            msg = 'Errata advisory {0} not allowed to trigger ' \
+                  'rebuilds.'.format(event.errata_id)
+            log.info(msg)
             return []
 
         # Generate the Database representation of `event`.
         db_event = Event.get_or_create(
             db.session, event.msg_id, event.search_key, event.__class__,
-            released=False)
+            released=False, manual=event.manual)
         db.session.commit()
         self.set_context(db_event)
 
@@ -177,7 +180,7 @@ class ErrataAdvisoryRPMsSignedHandler(BaseHandler):
         errata_id = int(db_event.search_key)
 
         packages = []
-        errata = Errata(conf.errata_tool_server_url)
+        errata = Errata()
         builds = errata.get_builds(errata_id)
         compose_source = None
         for nvr in builds:
@@ -495,7 +498,7 @@ class ErrataAdvisoryRPMsSignedHandler(BaseHandler):
 
         image_name = koji.parse_NVR(image["brew"]["build"])['name']
 
-        if not self.allow_build(
+        if not self.event.manual and not self.allow_build(
                 ArtifactType.IMAGE, image_name=image_name):
             log.info("Skipping rebuild of image %s, not allowed by "
                      "configuration", image_name)
@@ -512,7 +515,7 @@ class ErrataAdvisoryRPMsSignedHandler(BaseHandler):
 
         :param int errata_id: Errata ID.
         """
-        errata = Errata(conf.errata_tool_server_url)
+        errata = Errata()
         errata_id = int(errata_id)
 
         # Use the errata_id to find out Pulp repository IDs from Errata Tool
diff --git a/freshmaker/handlers/errata/errata_advisory_state_changed.py b/freshmaker/handlers/errata/errata_advisory_state_changed.py
index cc51a47..a33cf5a 100644
--- a/freshmaker/handlers/errata/errata_advisory_state_changed.py
+++ b/freshmaker/handlers/errata/errata_advisory_state_changed.py
@@ -19,7 +19,7 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 # SOFTWARE.
 
-from freshmaker import db, conf, log
+from freshmaker import db, log
 from freshmaker.events import (
     ErrataAdvisoryStateChangedEvent, ErrataAdvisoryRPMsSignedEvent)
 from freshmaker.models import Event, EVENT_TYPES
@@ -82,7 +82,7 @@ class ErrataAdvisoryStateChangedHandler(BaseHandler):
             return []
 
         # Get additional info from Errata to fill in the needed data.
-        errata = Errata(conf.errata_tool_server_url)
+        errata = Errata()
         advisories = errata.advisories_from_event(event)
         if not advisories:
             log.error("Unknown Errata advisory %d" % errata_id)
diff --git a/freshmaker/handlers/internal/__init__.py b/freshmaker/handlers/internal/__init__.py
new file mode 100644
index 0000000..4cd6e36
--- /dev/null
+++ b/freshmaker/handlers/internal/__init__.py
@@ -0,0 +1,22 @@
+# -*- 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.
+
+from .manual_rebuild import FreshmakerManualRebuildHandler  # noqa
diff --git a/freshmaker/handlers/internal/manual_rebuild.py b/freshmaker/handlers/internal/manual_rebuild.py
new file mode 100644
index 0000000..5b0dd58
--- /dev/null
+++ b/freshmaker/handlers/internal/manual_rebuild.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017  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 Jan Kaluza <jkaluza@redhat.com>
+
+from freshmaker import db, log
+from freshmaker.models import Event, EVENT_TYPES
+from freshmaker.handlers import ContainerBuildHandler
+from freshmaker.events import (
+    FreshmakerManualRebuildEvent, ErrataAdvisoryRPMsSignedEvent)
+from freshmaker.errata import Errata
+
+__all__ = ('FreshmakerManualRebuildHandler',)
+
+
+class FreshmakerManualRebuildHandler(ContainerBuildHandler):
+    """Start image rebuild with this compose containing included packages"""
+
+    def can_handle(self, event):
+        if not isinstance(event, FreshmakerManualRebuildEvent):
+            return False
+        return True
+
+    def rebuild_advisory_if_not_exists(self, event, errata_id):
+        """
+        Initiates rebuild of artifacts based on Errata advisory with
+        `errata_id` id.
+
+        :rtype: List of ErrataAdvisoryRPMsSignedEvent instances.
+        :return: List of extra events generated to initiate the rebuild.
+        """
+
+        db_event = db.session.query(Event).filter_by(
+            event_type_id=EVENT_TYPES[ErrataAdvisoryRPMsSignedEvent],
+            search_key=str(errata_id)).first()
+        if db_event:
+            log.info("Ignoring Errata advisory %d - it already exists in "
+                      "Freshmaker db.", errata_id)
+            return []
+
+        # Get additional info from Errata to fill in the needed data.
+        errata = Errata()
+        advisories = errata.advisories_from_event(event)
+        if not advisories:
+            log.error("Unknown Errata advisory %d" % errata_id)
+            return []
+
+        log.info("Generating ErrataAdvisoryRPMsSignedEvent for Errata "
+                 "advisory %d - manually triggered rebuild.", errata_id)
+        advisory = advisories[0]
+        new_event = ErrataAdvisoryRPMsSignedEvent(
+            event.msg_id + "." + str(advisory.name), advisory.name,
+            advisory.errata_id, advisory.security_impact)
+        new_event.manual = True
+        return [new_event]
+
+    def handle(self, event):
+        extra_events = []
+
+        if event.errata_id:
+            extra_events += self.rebuild_advisory_if_not_exists(event, event.errata_id)
+
+        return extra_events
diff --git a/freshmaker/messaging.py b/freshmaker/messaging.py
index f8f5ab4..8c4b290 100644
--- a/freshmaker/messaging.py
+++ b/freshmaker/messaging.py
@@ -40,7 +40,7 @@ def publish(topic, msg):
     :return: the value returned from underlying backend "send" method.
     """
     try:
-        handler = _messaging_backends[conf.messaging]['publish']
+        handler = _messaging_backends[conf.messaging_sender]['publish']
     except KeyError:
         raise KeyError("No messaging backend found for %r" % conf.messaging)
     return handler(topic, msg)
diff --git a/freshmaker/migrations/versions/2f5a2f4385a0_add_manual_triggered_to_event_model.py b/freshmaker/migrations/versions/2f5a2f4385a0_add_manual_triggered_to_event_model.py
new file mode 100644
index 0000000..1a3ff15
--- /dev/null
+++ b/freshmaker/migrations/versions/2f5a2f4385a0_add_manual_triggered_to_event_model.py
@@ -0,0 +1,26 @@
+"""Add manual_triggered to Event model
+
+Revision ID: 2f5a2f4385a0
+Revises: 2acc88805404
+Create Date: 2017-11-01 14:03:02.555397
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2f5a2f4385a0'
+down_revision = '2acc88805404'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.add_column('events', sa.Column('manual_triggered', sa.Boolean()))
+    ### end Alembic commands ###
+
+
+def downgrade():
+    ### commands auto generated by Alembic - please adjust! ###
+    op.drop_column('events', 'manual_triggered')
+    ### end Alembic commands ###
diff --git a/freshmaker/models.py b/freshmaker/models.py
index e8e5a86..dbc9387 100644
--- a/freshmaker/models.py
+++ b/freshmaker/models.py
@@ -127,8 +127,14 @@ class Event(FreshmakerBase):
         default=None,
         doc='Used to include new version packages to rebuild docker images')
 
+    manual_triggered = db.Column(
+        db.Boolean,
+        default=False,
+        doc='Whether this event is triggered manually')
+
     @classmethod
-    def create(cls, session, message_id, search_key, event_type, released=True):
+    def create(cls, session, message_id, search_key, event_type,
+               released=True, manual=False):
         if event_type in EVENT_TYPES:
             event_type = EVENT_TYPES[event_type]
         event = cls(
@@ -136,6 +142,7 @@ class Event(FreshmakerBase):
             search_key=search_key,
             event_type_id=event_type,
             released=released,
+            manual_triggered=manual,
         )
         session.add(event)
         return event
@@ -145,11 +152,13 @@ class Event(FreshmakerBase):
         return session.query(cls).filter_by(message_id=message_id).first()
 
     @classmethod
-    def get_or_create(cls, session, message_id, search_key, event_type, released=True):
+    def get_or_create(cls, session, message_id, search_key, event_type,
+                      released=True, manual=False):
         instance = cls.get(session, message_id)
         if instance:
             return instance
-        return cls.create(session, message_id, search_key, event_type, released)
+        return cls.create(session, message_id, search_key, event_type,
+                          released=released, manual=manual)
 
     @classmethod
     def get_unreleased(cls, session):
diff --git a/freshmaker/parsers/internal/__init__.py b/freshmaker/parsers/internal/__init__.py
new file mode 100644
index 0000000..9d44c11
--- /dev/null
+++ b/freshmaker/parsers/internal/__init__.py
@@ -0,0 +1,22 @@
+# -*- 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.
+
+from .manual_rebuild import FreshmakerManualRebuildParser  # noqa
diff --git a/freshmaker/parsers/internal/manual_rebuild.py b/freshmaker/parsers/internal/manual_rebuild.py
new file mode 100644
index 0000000..c698fd8
--- /dev/null
+++ b/freshmaker/parsers/internal/manual_rebuild.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017  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.
+
+from freshmaker.parsers import BaseParser
+from freshmaker.events import FreshmakerManualRebuildEvent
+
+
+class FreshmakerManualRebuildParser(BaseParser):
+    """Parser parsing odcs.compose.state.change"""
+
+    name = "FreshmakerManualRebuildEvent"
+    topic_suffixes = ["freshmaker.manual.rebuild"]
+
+    def can_parse(self, topic, msg):
+        return any([topic.endswith(s) for s in self.topic_suffixes])
+
+    def parse(self, topic, msg):
+        msg_id = msg.get('msg_id')
+        inner_msg = msg.get('msg')
+        errata_id = inner_msg.get('errata_id')
+
+        return FreshmakerManualRebuildEvent(msg_id, errata_id=errata_id)
diff --git a/freshmaker/views.py b/freshmaker/views.py
index 8b33737..e3c83f4 100644
--- a/freshmaker/views.py
+++ b/freshmaker/views.py
@@ -32,6 +32,8 @@ from freshmaker.api_utils import pagination_metadata
 from freshmaker.api_utils import filter_artifact_builds
 from freshmaker.api_utils import filter_events
 from freshmaker.api_utils import json_error
+from freshmaker.auth import login_required, requires_role
+from freshmaker import messaging
 
 api_v1 = {
     'event_types': {
@@ -108,6 +110,12 @@ api_v1 = {
                 'methods': ['GET'],
             }
         },
+        'manual_trigger': {
+            'url': '/api/1/builds/',
+            'options': {
+                'methods': ['POST'],
+            }
+        },
     },
 }
 
@@ -214,6 +222,18 @@ class BuildAPI(MethodView):
             else:
                 return json_error(404, "Not Found", "No such build found.")
 
+    @login_required
+    @requires_role('admins')
+    def post(self):
+        """Trigger image rebuild"""
+        data = request.get_json(force=True)
+        if 'errata_id' not in data:
+            return json_error(
+                400, 'Bad Request', 'Missing errata_id in request')
+
+        messaging.publish("manual.rebuild", data)
+        return jsonify(data), 200
+
 
 API_V1_MAPPING = {
     'events': EventAPI,
diff --git a/tests/test_errata_advisory_state_changed.py b/tests/test_errata_advisory_state_changed.py
index c63d217..2eb48d6 100644
--- a/tests/test_errata_advisory_state_changed.py
+++ b/tests/test_errata_advisory_state_changed.py
@@ -204,6 +204,8 @@ class TestAllowBuild(unittest.TestCase):
         """
 
         handler = ErrataAdvisoryRPMsSignedHandler()
+        handler.event = ErrataAdvisoryRPMsSignedEvent(
+            "123", "RHSA-2017", 123, "None")
 
         image = {"brew": {"build": "foo-1-2.3"}}
         ret = handler._filter_out_not_allowed_builds(image)
@@ -239,6 +241,8 @@ class TestAllowBuild(unittest.TestCase):
         """
 
         handler = ErrataAdvisoryRPMsSignedHandler()
+        handler.event = ErrataAdvisoryRPMsSignedEvent(
+            "123", "RHSA-2017", 123, "None")
 
         image = {"brew": {"build": "foo-1-2.3"}}
         ret = handler._filter_out_not_allowed_builds(image)
diff --git a/tests/test_freshmaker_manual_rebuild_handler.py b/tests/test_freshmaker_manual_rebuild_handler.py
new file mode 100644
index 0000000..8570c4d
--- /dev/null
+++ b/tests/test_freshmaker_manual_rebuild_handler.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2017  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 Chenxiong Qi <cqi@redhat.com>
+
+import unittest
+
+from mock import patch
+
+from freshmaker.handlers.internal import FreshmakerManualRebuildHandler
+from freshmaker.events import (
+    FreshmakerManualRebuildEvent, ErrataAdvisoryRPMsSignedEvent)
+
+from freshmaker.errata import ErrataAdvisory
+from freshmaker import db
+from freshmaker.models import Event
+
+
+class TestFreshmakerManualRebuildHandler(unittest.TestCase):
+
+    def setUp(self):
+        db.session.remove()
+        db.drop_all()
+        db.create_all()
+        db.session.commit()
+
+    def tearDown(self):
+        db.session.remove()
+        db.drop_all()
+        db.session.commit()
+
+    @patch('freshmaker.errata.Errata.advisories_from_event')
+    def test_rebuild_if_not_exists(self, advisories_from_event):
+        handler = FreshmakerManualRebuildHandler()
+
+        advisories_from_event.return_value = [
+            ErrataAdvisory(123, "RHSA-2017", "REL_PREP", "Critical")]
+        ev = FreshmakerManualRebuildEvent("msg123", errata_id=123)
+        ret = handler.handle(ev)
+
+        self.assertEqual(len(ret), 1)
+        self.assertEqual(ret[0].errata_id, 123)
+        self.assertEqual(ret[0].security_impact, "Critical")
+        self.assertEqual(ret[0].errata_name, "RHSA-2017")
+
+    @patch('freshmaker.errata.Errata.advisories_from_event')
+    def test_rebuild_if_not_exists_already_exists(
+            self, advisories_from_event):
+        handler = FreshmakerManualRebuildHandler()
+
+        Event.create(
+            db.session, "msg124", "123", ErrataAdvisoryRPMsSignedEvent)
+        db.session.commit()
+
+        advisories_from_event.return_value = [
+            ErrataAdvisory(123, "RHSA-2017", "REL_PREP", "Critical")]
+        ev = FreshmakerManualRebuildEvent("msg123", errata_id=123)
+        ret = handler.handle(ev)
+
+        self.assertEqual(len(ret), 0)
+
+    @patch('freshmaker.errata.Errata.advisories_from_event')
+    def test_rebuild_if_not_exists_unknown_errata_id(
+            self, advisories_from_event):
+        advisories_from_event.return_value = []
+        handler = FreshmakerManualRebuildHandler()
+
+        ev = FreshmakerManualRebuildEvent("msg123", errata_id=123)
+        ret = handler.handle(ev)
+
+        self.assertEqual(len(ret), 0)
diff --git a/tests/test_messaging.py b/tests/test_messaging.py
index a1b45ca..ee34b57 100644
--- a/tests/test_messaging.py
+++ b/tests/test_messaging.py
@@ -52,23 +52,23 @@ class TestSelectMessagingBackend(unittest.TestCase):
         }
         with patch.dict('freshmaker.messaging._messaging_backends',
                         mock_messaging_backends):
-            with patch.object(conf, 'messaging', new='fedmsg'):
+            with patch.object(conf, 'messaging_sender', new='fedmsg'):
                 publish('images.ready', fake_msg)
                 _fedmsg_publish.assert_called_once_with(
                     'images.ready', fake_msg)
 
-            with patch.object(conf, 'messaging', new='rhmsg'):
+            with patch.object(conf, 'messaging_sender', new='rhmsg'):
                 publish('images.ready', fake_msg)
                 _rhmsg_publish.assert_called_once_with(
                     'images.ready', fake_msg)
 
-            with patch.object(conf, 'messaging', new='in_memory'):
+            with patch.object(conf, 'messaging_sender', new='in_memory'):
                 publish('images.ready', fake_msg)
                 _in_memory_publish.assert_called_once_with(
                     'images.ready', fake_msg)
 
     def test_raise_error_if_backend_not_exists(self):
-        messaging_patcher = patch.object(conf, 'messaging', new='XXXX')
+        messaging_patcher = patch.object(conf, 'messaging_sender', new='XXXX')
         six.assertRaisesRegex(
             self, ValueError, 'Unsupported messaging system',
             messaging_patcher.start)
@@ -77,7 +77,7 @@ class TestSelectMessagingBackend(unittest.TestCase):
 class TestPublishToFedmsg(unittest.TestCase):
     """Test publish message to fedmsg using _fedmsg_publish backend"""
 
-    @patch.object(conf, 'messaging', new='fedmsg')
+    @patch.object(conf, 'messaging_sender', new='fedmsg')
     @patch.object(conf, 'messaging_backends',
                   new={'fedmsg': {'SERVICE': 'freshmaker'}})
     @patch('fedmsg.publish')
@@ -94,7 +94,7 @@ class TestPublishToFedmsg(unittest.TestCase):
 class TestPublishToRhmsg(unittest.TestCase):
     """Test publish message to UMB using _rhmsg_publish backend"""
 
-    @patch.object(conf, 'messaging', new='rhmsg')
+    @patch.object(conf, 'messaging_sender', new='rhmsg')
     @patch('rhmsg.activemq.producer.AMQProducer')
     @patch('proton.Message')
     def test_publish(self, Message, AMQProducer):
diff --git a/tests/test_views.py b/tests/test_views.py
index 2b95ec7..25aaa71 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -23,6 +23,8 @@ import unittest
 import json
 import six
 
+from mock import patch
+
 from freshmaker import app, db, events, models
 from freshmaker.types import ArtifactType, ArtifactBuildState
 
@@ -252,5 +254,31 @@ class TestViews(unittest.TestCase):
         self.assertEqual(data['message'], 'No such build state found.')
 
 
+class TestManualTriggerRebuild(unittest.TestCase):
+    def setUp(self):
+        db.session.remove()
+        db.drop_all()
+        db.create_all()
+        db.session.commit()
+
+        self.client = app.test_client()
+
+    def tearDown(self):
+
+        db.session.remove()
+        db.drop_all()
+        db.session.commit()
+
+    @patch('freshmaker.messaging.publish')
+    def test_manual_rebuild(self, publish):
+        resp = self.client.post('/api/1/builds/',
+                                data=json.dumps({'errata_id': 1}),
+                                content_type='application/json')
+        data = json.loads(resp.data.decode('utf-8'))
+
+        self.assertEqual(data["errata_id"], 1)
+        publish.assert_called_once_with('manual.rebuild', {u'errata_id': 1})
+
+
 if __name__ == '__main__':
     unittest.main()

