diff --git a/conf/config.py b/conf/config.py index eec13a8b..a9e0f3c3 100644 --- a/conf/config.py +++ b/conf/config.py @@ -120,7 +120,11 @@ class TestConfiguration(BaseConfiguration): RESOLVER = "db" ALLOWED_GROUPS_TO_IMPORT_MODULE = set(["mbs-import-module"]) - GREENWAVE_DECISION_CONTEXT = "osci_compose_gate_modules" + + # Greenwave configuration + GREENWAVE_URL = "https://greenwave.example.local/api/v1.0/" + GREENWAVE_DECISION_CONTEXT = "test_dec_context" + GREENWAVE_SUBJECT_TYPE = "some-module" STREAM_SUFFIXES = {r"^el\d+\.\d+\.\d+\.z$": 0.1} diff --git a/module_build_service/builder/KojiContentGenerator.py b/module_build_service/builder/KojiContentGenerator.py index 3fe9da81..9d9c035c 100644 --- a/module_build_service/builder/KojiContentGenerator.py +++ b/module_build_service/builder/KojiContentGenerator.py @@ -295,14 +295,9 @@ class KojiContentGenerator(object): return rpms def _get_build(self): - ret = {} - ret[u"name"] = self.module.name + ret = self.module.nvr if self.devel: ret["name"] += "-devel" - ret[u"version"] = self.module.stream.replace("-", "_") - # Append the context to the version to make NVRs of modules unique in the event of - # module stream expansion - ret[u"release"] = "{0}.{1}".format(self.module.version, self.module.context) ret[u"source"] = self.module.scmurl ret[u"start_time"] = calendar.timegm(self.module.time_submitted.utctimetuple()) ret[u"end_time"] = calendar.timegm(self.module.time_completed.utctimetuple()) diff --git a/module_build_service/config.py b/module_build_service/config.py index d216334e..ababb83a 100644 --- a/module_build_service/config.py +++ b/module_build_service/config.py @@ -603,6 +603,22 @@ class Config(object): "corresponding suffix added to formatted stream version. " 'For example, {r"regexp": 0.1, ...}', }, + "greenwave_url": { + "type": str, + "default": "", + "desc": "The URL of the server where Greenwave is running (should include " + "the root of the API)" + }, + "greenwave_subject_type": { + "type": str, + "default": "", + "desc": "Subject type for Greenwave requests" + }, + "greenwave_timeout": { + "type": int, + "default": 60, + "desc": "Greenwave response timeout" + } } def __init__(self, conf_section_obj): diff --git a/module_build_service/errors.py b/module_build_service/errors.py index 0c3bfaa5..8d26c982 100644 --- a/module_build_service/errors.py +++ b/module_build_service/errors.py @@ -55,6 +55,10 @@ class StreamAmbigous(ValueError): pass +class GreenwaveError(RuntimeError): + pass + + def json_error(status, error, message): response = jsonify({"status": status, "error": error, "message": message}) response.status_code = status diff --git a/module_build_service/models.py b/module_build_service/models.py index f2dd181e..c23c8cff 100644 --- a/module_build_service/models.py +++ b/module_build_service/models.py @@ -34,6 +34,7 @@ from collections import OrderedDict from datetime import datetime import sqlalchemy +import kobo.rpmlib from flask import has_app_context from sqlalchemy import func, and_ from sqlalchemy.orm import lazyload @@ -601,6 +602,18 @@ class ModuleBuild(MBSBase): ) return [build.id for build in query.all()] + @property + def nvr(self): + return { + u"name": self.name, + u"version": self.stream.replace("-", "_"), + u"release": "{0}.{1}".format(self.version, self.context) + } + + @property + def nvr_string(self): + return kobo.rpmlib.make_nvr(self.nvr) + @classmethod def create( cls, diff --git a/module_build_service/utils/greenwave.py b/module_build_service/utils/greenwave.py new file mode 100644 index 00000000..282c49f5 --- /dev/null +++ b/module_build_service/utils/greenwave.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019 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 Valerij Maljulin + + +import requests +import json +from module_build_service import log, conf +from module_build_service.errors import GreenwaveError + + +class Greenwave(object): + def __init__(self): + """ + Initialize greenwave instance with config + """ + self.url = conf.greenwave_url + if not self.url: + raise GreenwaveError("No Greenwave URL set") + self._decision_context = conf.greenwave_decision_context + if not self.decision_context: + raise GreenwaveError("No Greenwave decision context set") + self._subj_type = conf.greenwave_subject_type + self._gw_timeout = conf.greenwave_timeout + + def query_decision(self, build, prod_version): + """ + Query decision to greenwave + :param build: build object + :type build: module_build_service.models.ModuleBuild + :param prod_version: The product version string used for querying WaiverDB + :type prod_version: str + :return: response + :rtype: dict + """ + payload = { + "decision_context": self.decision_context, + "product_version": prod_version, + "subject_type": self.subject_type, + "subject_identifier": build.nvr_string + } + url = "{0}/decision".format(self.url) + headers = {"Content-Type": "application/json"} + try: + response = requests.post( + url=url, headers=headers, data=json.dumps(payload), timeout=self.timeout) + except requests.exceptions.Timeout: + raise GreenwaveError("Greenwave request timed out") + except Exception as exc: + log.exception(str(exc)) + raise GreenwaveError("Greenwave request error") + + try: + resp_json = response.json() + except ValueError: + log.debug("Greenwave response content (status {0}): {1}".format( + response.status_code, response.text)) + raise GreenwaveError("Greenwave returned invalid JSON.") + + log.debug('Query to Greenwave result: status=%d, content="%s"', + (response.status_code, resp_json)) + + if response.status_code == 200: + return resp_json + + try: + err_msg = resp_json["message"] + except KeyError: + err_msg = response.text + raise GreenwaveError("Greenwave returned {0} status code. Message: {1}".format( + response.status_code, err_msg)) + + @property + def url(self): + return self._url + + @url.setter + def url(self, value): + value = value.rstrip("/") + if value: + self._url = value + + @property + def decision_context(self): + return self._decision_context + + @property + def subject_type(self): + return self._subj_type + + @property + def timeout(self): + return self._gw_timeout + + @timeout.setter + def timeout(self, value): + self._gw_timeout = value diff --git a/tests/test_scheduler/test_greenwave.py b/tests/test_scheduler/test_greenwave.py index a3e6bc1d..976bd1f0 100644 --- a/tests/test_scheduler/test_greenwave.py +++ b/tests/test_scheduler/test_greenwave.py @@ -98,14 +98,14 @@ class TestDecisionUpdateHandler: log.debug.assert_called_once_with( 'Skip Greenwave message %s as MBS only handles messages with the decision context "%s"', "msg-id-1", - "osci_compose_gate_modules", + "test_dec_context" ) @patch("module_build_service.scheduler.handlers.greenwave.log") def test_not_satisfy_policies(self, log): msg = Mock( msg_id="msg-id-1", - decision_context="osci_compose_gate_modules", + decision_context="test_dec_context", policies_satisfied=False, subject_identifier="pkg-0.1-1.c1", ) @@ -144,7 +144,7 @@ class TestDecisionUpdateHandler: "msg_id": "msg-id-1", "topic": "org.fedoraproject.prod.greenwave.decision.update", "msg": { - "decision_context": "osci_compose_gate_modules", + "decision_context": "test_dec_context", "policies_satisfied": True, "subject_identifier": "pkg-0.1-1.c1", }, diff --git a/tests/test_utils/test_greenwave.py b/tests/test_utils/test_greenwave.py new file mode 100644 index 00000000..f41a65cf --- /dev/null +++ b/tests/test_utils/test_greenwave.py @@ -0,0 +1,70 @@ +# Copyright (c) 2019 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 Valerij Maljulin + + +import json +from mock import patch, Mock +import module_build_service.utils.greenwave +from tests import make_module + + +class TestGreenwaveQuery(): + @patch("module_build_service.utils.greenwave.requests") + def test_greenwave_decision(self, mock_requests): + resp_status = 200 + resp_content = { + "applicable_policies": ["osci_compose_modules"], + "policies_satisfied": True, + "satisfied_requirements": [ + { + "result_id": 7336633, + "testcase": "test-ci.test-module.tier1", + "type": "test-result-passed" + }, + { + "result_id": 7336650, + "testcase": "test-ci.test-module.tier2", + "type": "test-result-passed" + } + ], + "summary": "All required tests passed", + "unsatisfied_requirements": [] + } + response = Mock() + response.json.return_value = resp_content + response.status_code = resp_status + mock_requests.post.return_value = response + + fake_build = make_module("pkg:0.1:1:c1", requires_list={"platform": "el8"}) + + gw = module_build_service.utils.greenwave.Greenwave() + got_response = gw.query_decision(fake_build, prod_version="xxxx-8") + + assert got_response == resp_content + assert json.loads(mock_requests.post.call_args_list[0][1]["data"]) == { + "decision_context": "test_dec_context", + "product_version": "xxxx-8", "subject_type": "some-module", + "subject_identifier": "pkg-0.1-1.c1"} + assert mock_requests.post.call_args_list[0][1]["headers"] == { + "Content-Type": "application/json"} + assert mock_requests.post.call_args_list[0][1]["url"] == \ + "https://greenwave.example.local/api/v1.0/decision"