#22 Add the pdc_modules toddler
Merged 3 years ago by nphilipp. Opened 3 years ago by zlopez.
fedora-infra/ zlopez/toddlers pdc_import_modules  into  master

file modified
+1
@@ -6,4 +6,5 @@ 

              vars:

                tox_envlist: ALL

                tox_install_siblings: false

+             pre-run: ci/prepare.yaml

  ...

file added
+10
@@ -0,0 +1,10 @@ 

+  - hosts: all

+    tasks:

+      - name: Install dependencies

+        dnf:

+          name:

+            - cairo-devel

+            - cairo-gobject-devel

+            - gobject-introspection-devel

+            - libmodulemd1

+          state: latest

file modified
+2
@@ -1,7 +1,9 @@ 

+ beanbag

  bs4

  fedora-messaging

  koji

  requests

+ pyGObject

  python-fedora

  python-bugzilla>=2.4.0

  pdc-client

@@ -0,0 +1,532 @@ 

+ import logging

+ from unittest.mock import call, MagicMock, Mock, patch

+ 

+ import beanbag

+ import fedora_messaging.api

+ import koji

+ import pytest

+ 

+ import toddlers.plugins.pdc_modules as pdc_modules

+ 

+ 

+ class TestPDCModulesToddler:

+ 

+     toddler_cls = pdc_modules.PDCModules

+ 

+     def test_accepts_topic_invalid(self, toddler):

+         assert toddler.accepts_topic("foo.bar") is False

+ 

+     @pytest.mark.parametrize(

+         "topic",

+         [

+             "org.fedoraproject.*.mbs.module.state.change",

+             "org.fedoraproject.stg.mbs.module.state.change",

+             "org.fedoraproject.prod.mbs.module.state.change",

+         ],

+     )

+     def test_accepts_topic_valid(self, topic, toddler):

+         assert toddler.accepts_topic(topic)

+ 

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_pdc_api_exception(self, pdc, toddler):

+         """

+         Assert that exception is raised when pdc_api can't be retrieved.

+         """

+         client = MagicMock()

+ 

+         res = Mock()

+         res.status_code = 500

+ 

+         client["modules"]._.side_effect = beanbag.BeanBagException(res, "")

+         pdc.return_value = client

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {}

+ 

+         with pytest.raises(beanbag.BeanBagException) as e:

+             toddler.process({"config": "foobar"}, msg)

+             assert e.status_code == 500

+ 

+         client["modules"]._.assert_called_with(page_size=1)

+ 

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_invalid_state(self, pdc, caplog, toddler):

+         caplog.set_level(logging.INFO)

+         client = MagicMock()

+         pdc.return_value = client

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {"state_name": "invalid"}

+ 

+         toddler.process({"config": "foobar"}, msg)

+ 

+         client["modules"]._.assert_called_with(page_size=1)

+ 

+         assert len(caplog.records) == 1

+         assert (

+             caplog.records[-1].message

+             == "We are not interested in this state: 'invalid', skip"

+         )

+ 

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_existing_module_build(self, pdc, toddler):

+         """

+         Assert that the PDC is correctly updated when state is changed to build.

+         """

+         client = MagicMock()

+         pdc_api = "modules"

+         pdc.return_value = client

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "state_name": "build",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+             "koji_tag": "tag",

+         }

+         uid = "module:6:6.1:00000000"

+ 

+         toddler.process({"config": "foobar"}, msg)

+ 

+         print(client[pdc_api]._.mock_calls)

+ 

+         assert call(page_size=1) in client[pdc_api]._.mock_calls

+         assert call(page_size=-1, uid=uid) in client[pdc_api]._.mock_calls

+ 

+         assert client[pdc_api][123].mock_calls == [call._.__iadd__({"koji_tag": "tag"})]

+ 

+     @patch("toddlers.plugins.pdc_modules.koji")

+     @patch("toddlers.plugins.pdc_modules.Modulemd")

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_existing_module_ready(self, pdc, modulemd, koji, toddler):

+         """

+         Assert that the PDC is correctly updated when state is changed to ready.

+         """

+         client = MagicMock()

+         pdc_api = "modules"

+         pdc.return_value = client

+ 

+         mmd = Mock()

+ 

+         modulemd.Module.new_from_string.return_value = mmd

+ 

+         session = Mock()

+         session.listTaggedRPMS.return_value = (

+             [

+                 {

+                     "build_id": 123,

+                     "name": "rpm",

+                     "version": 1.0,

+                     "release": 1,

+                     "epoch": 0,

+                     "arch": "x86_64",

+                 }

+             ],

+             [{"build_id": 123, "name": "rpm", "nvr": "rpm-1.0-1"}],

+         )

+         koji.ClientSession.return_value = session

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "state_name": "ready",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+         }

+         uid = "module:6:6.1:00000000"

+ 

+         config = {"koji_url": "url"}

+ 

+         toddler.process(config, msg)

+ 

+         assert call(page_size=1) in client[pdc_api]._.mock_calls

+         assert call(page_size=-1, uid=uid) in client[pdc_api]._.mock_calls

+ 

+         assert client[pdc_api][123].mock_calls == [

+             call._.__iadd__(

+                 {

+                     "active": True,

+                     "rpms": [

+                         {

+                             "name": "rpm",

+                             "version": 1.0,

+                             "release": 1,

+                             "epoch": 0,

+                             "arch": "x86_64",

+                             "srpm_name": "rpm",

+                             "srpm_nevra": "rpm-1.0-1",

+                         }

+                     ],

+                 }

+             )

+         ]

+ 

+         assert mmd.upgrade.call_count == 1

+ 

+     @patch("toddlers.plugins.pdc_modules.koji")

+     @patch("toddlers.plugins.pdc_modules.Modulemd")

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_existing_module_ready_src_arch(self, pdc, modulemd, koji, toddler):

+         """

+         Assert that the PDC is correctly updated when state is changed to ready and the module has

+         src arch.

+         """

+         client = MagicMock()

+         pdc_api = "modules"

+         pdc.return_value = client

+ 

+         mmd_rpm = Mock()

+         mmd_rpm.get_ref.return_value = "foo"

+ 

+         mmd = Mock()

+         mmd.get_rpm_components.return_value = {"rpm": mmd_rpm}

+         mmd.get_xmd.return_value = {"mbs": {"rpms": {"rpm": {"ref": "bar"}}}}

+ 

+         modulemd.Module.new_from_string.return_value = mmd

+ 

+         session = Mock()

+         session.listTaggedRPMS.return_value = (

+             [

+                 {

+                     "build_id": 123,

+                     "name": "rpm",

+                     "version": 1.0,

+                     "release": 1,

+                     "epoch": 0,

+                     "arch": "src",

+                 }

+             ],

+             [{"build_id": 123, "name": "rpm", "nvr": "rpm-1.0-1"}],

+         )

+         koji.ClientSession.return_value = session

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "state_name": "ready",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+         }

+         uid = "module:6:6.1:00000000"

+ 

+         config = {"koji_url": "url"}

+ 

+         toddler.process(config, msg)

+ 

+         assert call(page_size=1) in client[pdc_api]._.mock_calls

+         assert call(page_size=-1, uid=uid) in client[pdc_api]._.mock_calls

+ 

+         assert client[pdc_api][123].mock_calls == [

+             call._.__iadd__(

+                 {

+                     "active": True,

+                     "rpms": [

+                         {

+                             "name": "rpm",

+                             "version": 1.0,

+                             "release": 1,

+                             "epoch": 0,

+                             "arch": "src",

+                             "srpm_name": "rpm",

+                             "srpm_commit_hash": "bar",

+                             "srpm_commit_branch": "foo",

+                         }

+                     ],

+                 }

+             )

+         ]

+ 

+         assert mmd.upgrade.call_count == 1

+ 

+     @patch("toddlers.plugins.pdc_modules.koji.ClientSession")

+     @patch("toddlers.plugins.pdc_modules.Modulemd")

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_existing_module_ready_koji_error(

+         self, pdc, modulemd, koji_client, toddler

+     ):

+         """

+         Assert that the PDC is correctly updated when state is changed to ready and koji error

+         is encountered.

+         """

+         client = MagicMock()

+         pdc_api = "modules"

+         pdc.return_value = client

+ 

+         mmd = Mock()

+ 

+         modulemd.Module.new_from_string.return_value = mmd

+ 

+         session = Mock()

+         session.listTaggedRPMS.side_effect = koji.AuthError()

+         koji_client.return_value = session

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "state_name": "ready",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+         }

+         uid = "module:6:6.1:00000000"

+ 

+         config = {"koji_url": "url"}

+ 

+         toddler.process(config, msg)

+ 

+         assert call(page_size=1) in client[pdc_api]._.mock_calls

+         assert call(page_size=-1, uid=uid) in client[pdc_api]._.mock_calls

+ 

+         assert client[pdc_api][123].mock_calls == [

+             call._.__iadd__({"active": True, "rpms": []})

+         ]

+ 

+         assert mmd.upgrade.call_count == 1

+ 

+     @patch("toddlers.plugins.pdc_modules.koji")

+     @patch("toddlers.plugins.pdc_modules.Modulemd")

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_new_module_ready(self, pdc, modulemd, koji, toddler):

+         """

+         Assert that the PDC is correctly updated when state is changed and module doesn't exist.

+         """

+         client = MagicMock()

+         pdc_api = "modules"

+         uid = "module:6:6.1:00000000"

+         client[pdc_api]._.side_effect = (

+             "",

+             [],

+             {"uid": uid, "modulemd": "module", "koji_tag": "NA"},

+         )

+         pdc.return_value = client

+ 

+         deps = Mock()

+         stream = Mock()

+         stream.get.return_value = ["stream"]

+         deps.get_requires.return_value = {

+             "dependency": stream,

+         }

+         deps.get_buildrequires.return_value = {

+             "dependency": stream,

+         }

+ 

+         mmd = Mock()

+         mmd.get_dependencies.return_value = [deps]

+ 

+         modulemd.Module.new_from_string.return_value = mmd

+ 

+         session = Mock()

+         session.listTaggedRPMS.return_value = (

+             [

+                 {

+                     "build_id": 123,

+                     "name": "rpm",

+                     "version": 1.0,

+                     "release": 1,

+                     "epoch": 0,

+                     "arch": "x86_64",

+                 }

+             ],

+             [{"build_id": 123, "name": "rpm", "nvr": "rpm-1.0-1"}],

+         )

+         koji.ClientSession.return_value = session

+ 

+         req = Mock()

+         resp_mbs_module = Mock()

+         resp_mbs_module.json.return_value = {

+             "modulemd": "module",

+         }

+         req.get.return_value = resp_mbs_module

+         toddler.requests_session = req

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "id": 123,

+             "state_name": "ready",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+         }

+ 

+         config = {"koji_url": "koji_url", "mbs_url": "mbs_url"}

+ 

+         toddler.process(config, msg)

+ 

+         resp_mbs_module.raise_for_status.assert_called_once()

+         req.get.assert_called_with("mbs_url/123?verbose=True", timeout=30)

+ 

+         assert client[pdc_api]._.mock_calls == [

+             call(page_size=1),

+             call(page_size=-1, uid=uid),

+             call(

+                 {

+                     "name": "module",

+                     "stream": 6,

+                     "version": 6.1,

+                     "context": "00000000",

+                     "koji_tag": "NA",

+                     "runtime_deps": [{"dependency": "dependency", "stream": "stream"}],

+                     "build_deps": [{"dependency": "dependency", "stream": "stream"}],

+                     "modulemd": "module",

+                 }

+             ),

+         ]

+ 

+         assert client[pdc_api][123].mock_calls == [

+             call._.__iadd__(

+                 {

+                     "active": True,

+                     "rpms": [

+                         {

+                             "name": "rpm",

+                             "version": 1.0,

+                             "release": 1,

+                             "epoch": 0,

+                             "arch": "x86_64",

+                             "srpm_name": "rpm",

+                             "srpm_nevra": "rpm-1.0-1",

+                         }

+                     ],

+                 }

+             )

+         ]

+ 

+         assert mmd.upgrade.call_count == 2

+ 

+     @patch("toddlers.plugins.pdc_modules.koji")

+     @patch("toddlers.plugins.pdc_modules.Modulemd")

+     @patch("toddlers.plugins.pdc_modules.pdc_client_for_config")

+     def test_process_new_module_ready_different_pdc_api(

+         self, pdc, modulemd, koji, toddler

+     ):

+         """

+         Assert that the PDC is correctly updated when state is changed, module doesn't exist and

+         different pdc_api "unreleasedvariants" is used.

+         """

+         client = MagicMock()

+         pdc_api = "unreleasedvariants"

+ 

+         res = Mock()

+         res.status_code = 404

+ 

+         uid = "module:6:6.1"

+         module_mock = MagicMock()

+         module_mock._.side_effect = (

+             beanbag.BeanBagException(res, ""),

+             [],

+             {"variant_uid": uid, "modulemd": "module", "koji_tag": "NA"},

+         )

+ 

+         client.__getitem__.return_value = module_mock

+         pdc.return_value = client

+ 

+         deps = Mock()

+         stream = Mock()

+         stream.get.return_value = ["stream"]

+         deps.get_requires.return_value = {

+             "dependency": stream,

+         }

+         deps.get_buildrequires.return_value = {

+             "dependency": stream,

+         }

+ 

+         mmd = Mock()

+         mmd.get_dependencies.return_value = [deps]

+ 

+         modulemd.Module.new_from_string.return_value = mmd

+ 

+         session = Mock()

+         session.listTaggedRPMS.return_value = (

+             [

+                 {

+                     "build_id": 123,

+                     "name": "rpm",

+                     "version": 1.0,

+                     "release": 1,

+                     "epoch": 0,

+                     "arch": "x86_64",

+                 }

+             ],

+             [{"build_id": 123, "name": "rpm", "nvr": "rpm-1.0-1"}],

+         )

+         koji.ClientSession.return_value = session

+ 

+         req = Mock()

+         resp_mbs_module = Mock()

+         resp_mbs_module.json.return_value = {

+             "modulemd": "module",

+         }

+         req.get.return_value = resp_mbs_module

+         toddler.requests_session = req

+ 

+         msg = fedora_messaging.api.Message()

+         msg.id = 123

+         msg.topic = "org.fedoraproject.prod.mbs.module.state.change"

+         msg.body = {

+             "id": 123,

+             "state_name": "ready",

+             "name": "module",

+             "stream": 6,

+             "version": 6.1,

+         }

+ 

+         config = {"koji_url": "koji_url", "mbs_url": "mbs_url"}

+ 

+         toddler.process(config, msg)

+ 

+         resp_mbs_module.raise_for_status.assert_called_once()

+         req.get.assert_called_with("mbs_url/123?verbose=True", timeout=30)

+ 

+         assert client[pdc_api]._.mock_calls == [

+             call(page_size=1),

+             call(page_size=-1, variant_uid=uid),

+             call(

+                 {

+                     "variant_id": "module",

+                     "variant_uid": uid,

+                     "variant_name": "module",

+                     "variant_version": 6,

+                     "variant_release": 6.1,

+                     "variant_type": "module",

+                     "koji_tag": "NA",

+                     "runtime_deps": [{"dependency": "dependency", "stream": "stream"}],

+                     "build_deps": [{"dependency": "dependency", "stream": "stream"}],

+                     "modulemd": "module",

+                 }

+             ),

+         ]

+ 

+         assert client[pdc_api][123].mock_calls == [

+             call._.__iadd__(

+                 {

+                     "active": True,

+                     "rpms": [

+                         {

+                             "name": "rpm",

+                             "version": 1.0,

+                             "release": 1,

+                             "epoch": 0,

+                             "arch": "x86_64",

+                             "srpm_name": "rpm",

+                             "srpm_nevra": "rpm-1.0-1",

+                         }

+                     ],

+                 }

+             )

+         ]

+ 

+         assert mmd.upgrade.call_count == 2

file modified
+2
@@ -122,6 +122,8 @@ 

  [consumer_config.pdc_import_compose]

  old_composes_url = "https://kojipkgs.fedoraproject.org/compose/"

  

+ [consumer_config.pdc_modules]

+ mbs_url = "https://mbs.fedoraproject.org/module-build-service/2/module-builds/"

  

  [qos]

  prefetch_size = 0

@@ -0,0 +1,249 @@ 

+ """

+ Reacts on state change message coming from MBS and either updates or creates

+  a new module in PDC.

+ 

+ This toddler replaces the respective pdc-updater handler:

+ https://github.com/fedora-infra/pdc-updater/blob/develop/pdcupdater/handlers/modules.py

+ 

+ Authors:    Michal Konecny <mkonecny@redhat.com>

+ 

+ """

+ 

+ import logging

+ 

+ import beanbag

+ import gi

+ import koji

+ 

+ from ..base import ToddlerBase

+ from ..utils.pdc import pdc_client_for_config

+ from ..utils.requests import make_session

+ 

+ gi.require_version("Modulemd", "1.0")

+ from gi.repository import Modulemd  # noqa: E402, I100

+ 

+ _log = logging.getLogger(__name__)

+ 

+ # These are the module states we care about

+ _STATES = [

+     "ready",

+     "build",

+ ]

+ 

+ 

+ class PDCModules(ToddlerBase):

+     """

+     Listens to messages sent by MBS to handle the changes

+     in module build states.

+     """

+ 

+     name = "pdc_modules"

+ 

+     amqp_topics = ["org.fedoraproject.*.mbs.module.state.change"]

+ 

+     def __init__(self):

+         self.requests_session = make_session()

+ 

+     def accepts_topic(self, topic):

+         """

+         Returns a boolean whether this toddler is interested in messages

+         from this specific topic.

+         """

+         return topic.startswith("org.fedoraproject.") and topic.endswith(

+             ("mbs.module.state.change")

+         )

+ 

+     def _get_pdc_api(self, pdc):

+         """Determine if the new "modules" API is available for use. This API

+         supersedes "unreleasedvariants" but provides backwards-compatible

+         data for it."""

+         try:

+             pdc["modules"]._(page_size=1)

+             return "modules"

+         except beanbag.BeanBagException as error:

+             if error.response.status_code == 404:

+                 return "unreleasedvariants"

+             else:

+                 raise

+ 

+     def process(self, config, message):

+         """ Process a given message. """

+         pdc = pdc_client_for_config(config)

+ 

+         _log.debug("Determining which PDC API to use.")

+         pdc_api = self._get_pdc_api(pdc)

+         _log.debug("Using the %s PDC API.", pdc_api)

+ 

+         state = message.body["state_name"]

+ 

+         # Only process messages with states we are interested in

+         if state not in _STATES:

+             _log.info("We are not interested in this state: '%s', skip", state)

+             return

+ 

+         module = self._get_or_create_module(config, pdc, pdc_api, message)

+ 

+         if pdc_api == "modules":

+             uid = module["uid"]

+         else:

+             uid = module["variant_uid"]

+ 

+         if state == "build":

+             # At this point we can update the Koji tag from MBS

+             pdc[pdc_api][uid]._ += {"koji_tag": message.body["koji_tag"]}

+         elif state == "ready":

+             _log.info("%r ready.  Patching with rpms and active=True.", uid)

+             rpms = self._get_module_rpms(config, pdc, module)

+             pdc[pdc_api][uid]._ += {"active": True, "rpms": rpms}

+ 

+     def _get_modulemd_by_mbs_id(self, config, idx):

+         """ Query MBS to get the modulemd """

+         mbs_url = config["mbs_url"]

+         module_url = "{0}/{1}?verbose=True".format(mbs_url, idx)

+         response = self.requests_session.get(module_url, timeout=30)

+         response.raise_for_status()

+         return response.json()["modulemd"]

+ 

+     def _create_module(self, config, pdc, pdc_api, message):

+         """Creates a module in PDC."""

+         modulemd = self._get_modulemd_by_mbs_id(config, message.body["id"])

+         mmd = Modulemd.Module.new_from_string(modulemd)

+         mmd.upgrade()

+ 

+         runtime_deps = []

+         build_deps = []

+         for deps in mmd.get_dependencies():

+             for dependency, streams in deps.get_requires().items():

+                 for stream in streams.get():

+                     runtime_deps.append({"dependency": dependency, "stream": stream})

+             for dependency, streams in deps.get_buildrequires().items():

+                 for stream in streams.get():

+                     build_deps.append({"dependency": dependency, "stream": stream})

+ 

+         name = message.body["name"]

+         stream = message.body["stream"]

+         version = message.body["version"]

+         # koji tag in pdc is required, but not available in init

+         # message, so let's put a placeholder value for now and updater later

+         koji_tag = "NA"

+ 

+         if pdc_api == "modules":

+             data = {

+                 "name": name,

+                 "stream": stream,

+                 "version": version,

+                 # Check if this was provided by MBS

+                 "context": message.body.get("context", "00000000"),

+             }

+         else:

+             data = {

+                 "variant_id": name,

+                 "variant_uid": self._get_uid(pdc_api, message),

+                 "variant_name": name,

+                 "variant_version": stream,

+                 "variant_release": version,

+                 "variant_type": "module",

+             }

+ 

+         data["koji_tag"] = koji_tag

+         data["runtime_deps"] = runtime_deps

+         data["build_deps"] = build_deps

+         data["modulemd"] = modulemd

+         module = pdc[pdc_api]._(data)

+ 

+         return module

+ 

+     def _get_or_create_module(self, config, pdc, pdc_api, message):

+         """Attempts to retrieve the corresponding module from PDC, or if it's

+         missing, creates it."""

+         uid = self._get_uid(pdc_api, message)

+         _log.info("Looking up module %r", uid)

+         if pdc_api == "modules":

+             query = {"uid": uid}

+         else:

+             query = {"variant_uid": uid}

+         modules = pdc[pdc_api]._(page_size=-1, **query)

+ 

+         if not modules:

+             _log.info("%r not found.  Creating.", uid)  # a new module!

+             return self._create_module(config, pdc, pdc_api, message)

+         else:

+             return modules[0]

+ 

+     def _get_uid(self, pdc_api, message):

+         """Returns the proper UID based on the message body and PDC API."""

+         name = message.body["name"]

+         stream = message.body["stream"]

+         version = message.body["version"]

+         uid = "{n}:{s}:{v}".format(n=name, s=stream, v=version)

+         if pdc_api == "modules":

+             # Check to see if the context was provided. Only MBS v1.6+ will

+             # provide this value.

+             context = message.body.get("context", "00000000")

+             uid = ":".join([uid, context])

+         return uid

+ 

+     def _koji_rpms_in_tag(self, url, tag):

+         """ Return the list of koji rpms in a tag. """

+         _log.info("Listing rpms in koji(%s) tag %s", url, tag)

+         session = koji.ClientSession(url)

+ 

+         try:

+             rpms, builds = session.listTaggedRPMS(tag, latest=True)

+         except koji.GenericError:

+             _log.exception("Failed to list rpms in tag %r", tag)

+             # If the tag doesn't exist.. then there are no rpms in that tag.

+             return []

+ 

+         # Extract some srpm-level info from the build attach it to each rpm

+         builds = {build["build_id"]: build for build in builds}

+         for rpm in rpms:

+             idx = rpm["build_id"]

+             rpm["srpm_name"] = builds[idx]["name"]

+             rpm["srpm_nevra"] = builds[idx]["nvr"]

+ 

+         return rpms

+ 

+     def _get_module_rpms(self, config, pdc, module):

+         """

+         Returns the list of rpms in the format of the "rpms" key for the

+         "modules" PDC endpoint. The list is obtained from the Koji tag defined

+         by the "koji_tag" propery of the input module.

+         """

+         koji_url = config["koji_url"]

+         mmd = Modulemd.Module.new_from_string(module["modulemd"])

+         mmd.upgrade()

+ 

+         koji_rpms = self._koji_rpms_in_tag(koji_url, module["koji_tag"])

+ 

+         rpms = []

+         # Flatten into a list and augment the koji dict with tag info.

+         for rpm in koji_rpms:

+             data = {

+                 "name": rpm["name"],

+                 "version": rpm["version"],

+                 "release": rpm["release"],

+                 "epoch": rpm["epoch"] or 0,

+                 "arch": rpm["arch"],

+                 "srpm_name": rpm["srpm_name"],

+             }

+ 

+             if "srpm_nevra" in rpm and rpm["arch"] != "src":

+                 data["srpm_nevra"] = rpm["srpm_nevra"]

+ 

+             # For SRPM packages, include the hash and branch from which is

+             # has been built.

+             if (

+                 rpm["arch"] == "src"

+                 and rpm["name"] in mmd.get_rpm_components().keys()

+                 and "rpms" in mmd.get_xmd()["mbs"].keys()

+                 and rpm["name"] in mmd.get_xmd()["mbs"]["rpms"]

+             ):

+                 mmd_rpm = mmd.get_rpm_components()[rpm["name"]]

+                 xmd_rpm = mmd.get_xmd()["mbs"]["rpms"][rpm["name"]]

+                 data["srpm_commit_hash"] = xmd_rpm["ref"]

+                 if xmd_rpm["ref"] != mmd_rpm.get_ref():

+                     data["srpm_commit_branch"] = mmd_rpm.get_ref()

+             rpms.append(data)

+ 

+         return rpms

This toddler reacts on state change message coming from MBS and either
updates or create a new module in PDC.

This toddler is the equivalent to the pdc-updater handlers:
https://github.com/fedora-infra/pdc-updater/blob/develop/pdcupdater/handlers/modules.py

Signed-off-by: Michal Konečný mkonecny@redhat.com

Build failed.

  • tox : FAILURE in 3m 47s

I will probably need to do a few adjustments to zuul, because of the pyGObject library

Metadata Update from @nphilipp:
- Request assigned

3 years ago

There are some import order issues the Zuul run flagged before it ran into the pycairo build failure:

2020-07-23 12:34:42.092423 | container | [437] /root/src/pagure.io/fedora-infra/toddlers$ /root/src/pagure.io/fedora-infra/toddlers/.tox/flake8/bin/flake8 .
2020-07-23 12:34:42.899651 | container | ./tests/plugins/test_pdc_modules.py:2:1: I101 Imported names are in the wrong order. Should be call, MagicMock, Mock, patch
2020-07-23 12:34:42.899721 | container | from unittest.mock import Mock, patch, MagicMock, call
2020-07-23 12:34:42.899728 | container | ^
2020-07-23 12:34:42.899733 | container | ./toddlers/plugins/pdc_modules.py:21:1: I100 Import statements are in the wrong order. 'import gi' should be before 'from ..utils.requests import make_session' and in a different group.
2020-07-23 12:34:42.899738 | container | import gi
2020-07-23 12:34:42.899742 | container | ^
2020-07-23 12:34:42.899745 | container | ./toddlers/plugins/pdc_modules.py:24:1: E402 module level import not at top of file
2020-07-23 12:34:42.899750 | container | from gi.repository import Modulemd
2020-07-23 12:34:42.899754 | container | ^
2020-07-23 12:34:42.899758 | container | ./toddlers/plugins/pdc_modules.py:24:1: I202 Additional newline in a group of imports. 'from gi.repository import Modulemd' is identified as Third Party and 'import gi' is identified as Third Party.
2020-07-23 12:34:42.899765 | container | from gi.repository import Modulemd
2020-07-23 12:34:42.899769 | container | ^
2020-07-23 12:34:42.926801 | container | ERROR: InvocationError for command /root/src/pagure.io/fedora-infra/toddlers/.tox/flake8/bin/flake8 . (exited with code 1)

I missed the unittest.mock import order, but the gi is special case. You need to first initialize it before importing from it.

rebased onto 457474230ed41097ec4efce440a54ae23d9f0b4d

3 years ago

Build failed.

  • tox : FAILURE in 2m 54s

1 new commit added

  • Update zuul
3 years ago

Build failed.

  • tox : FAILURE in 4m 38s

2 new commits added

  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Build failed.

  • tox : FAILURE in 4m 21s

2 new commits added

  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Move the import to where it would normally go, and don't use a naked noqa, but specify the error that's ignored. Like this:

diff --git a/toddlers/plugins/pdc_modules.py b/toddlers/plugins/pdc_modules.py
index fc5cac6..e19ac8c 100644
--- a/toddlers/plugins/pdc_modules.py
+++ b/toddlers/plugins/pdc_modules.py
@@ -13,15 +13,14 @@ import logging

 import beanbag
 import gi
+gi.require_version("Modulemd", "1.0")  # noqa: E402
+from gi.repository import Modulemd
 import koji

 from ..base import ToddlerBase
 from ..utils.pdc import pdc_client_for_config
 from ..utils.requests import make_session

-gi.require_version("Modulemd", "1.0")  # noqa
-from gi.repository import Modulemd  # noqa
-
 _log = logging.getLogger(__name__)

 # These are the module states we care about

The original # noqa was in modules.py, I just took it with myself.

Let me fix this.

Move the import to where it would normally go, and don't use a naked noqa, but specify the error that's ignored. Like this:
diff --git a/toddlers/plugins/pdc_modules.py b/toddlers/plugins/pdc_modules.py
index fc5cac6..e19ac8c 100644
--- a/toddlers/plugins/pdc_modules.py
+++ b/toddlers/plugins/pdc_modules.py
@@ -13,15 +13,14 @@ import logging

import beanbag
import gi
+gi.require_version("Modulemd", "1.0") # noqa: E402
+from gi.repository import Modulemd
import koji

from ..base import ToddlerBase
from ..utils.pdc import pdc_client_for_config
from ..utils.requests import make_session

-gi.require_version("Modulemd", "1.0") # noqa
-from gi.repository import Modulemd # noqa
-
_log = logging.getLogger(name)

# These are the module states we care about

I did the change and it results in this, because it introduces the E402 to every import line under it:

import logging

import beanbag
import gi
gi.require_version("Modulemd", "1.0")
from gi.repository import Modulemd  # noqa: E402
import koji  # noqa: E402

from ..base import ToddlerBase  # noqa: E402
from ..utils.pdc import pdc_client_for_config  # noqa: E402
from ..utils.requests import make_session  # noqa: E402

_log = logging.getLogger(__name__)

I think the best solution for this is:

import logging

import beanbag
import gi
import koji

from ..base import ToddlerBase
from ..utils.pdc import pdc_client_for_config
from ..utils.requests import make_session

gi.require_version("Modulemd", "1.0")
from gi.repository import Modulemd  # noqa: E402,I100

@nphilipp What do you think?

1 new commit added

  • Fix import error
3 years ago

Build succeeded.

  • tox : SUCCESS in 4m 46s

Zuul is now working :-)

The import statements should go in sorted order. It will work if you put # noqa: E402 on the line "breaking ranks", i.e. the one with gi.require_version(...):

...
import gi
gi.require_version("Modulemd", "1.0")  # noqa: E402
from gi.repository import Modulemd
import koji
...

3 new commits added

  • Fix import error
  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Build failed.

  • tox : FAILURE in 4m 34s

3 new commits added

  • Fix import error
  • Update zuul
  • Add the pdc_modules toddler
3 years ago

rebased onto 08258ce3d977a8a7acbc9235f8529a9b13695649

3 years ago

Build failed.

  • tox : FAILURE in 4m 41s

The import statements should go in sorted order. It will work if you put # noqa: E402 on the line "breaking ranks", i.e. the one with gi.require_version(...):
...
import gi
gi.require_version("Modulemd", "1.0") # noqa: E402
from gi.repository import Modulemd
import koji
...

This is the output from black after your change:

2020-07-23 16:50:39.098760 | container | [372] /root/src/pagure.io/fedora-infra/toddlers$ /root/src/pagure.io/fedora-infra/toddlers/.tox/black/bin/black --check --diff .
2020-07-23 16:50:39.871084 | container | --- toddlers/plugins/pdc_modules.py    2020-07-23 16:47:54.180420 +0000
2020-07-23 16:50:39.871135 | container | +++ toddlers/plugins/pdc_modules.py    2020-07-23 16:50:39.867843 +0000
2020-07-23 16:50:39.871142 | container | @@ -11,10 +11,11 @@
2020-07-23 16:50:39.871149 | container |
2020-07-23 16:50:39.871155 | container |  import logging
2020-07-23 16:50:39.871161 | container |
2020-07-23 16:50:39.871176 | container |  import beanbag
2020-07-23 16:50:39.871182 | container |  import gi
2020-07-23 16:50:39.871188 | container | +
2020-07-23 16:50:39.871194 | container |  gi.require_version("Modulemd", "1.0")  # noqa: E402
2020-07-23 16:50:39.871200 | container |  from gi.repository import Modulemd
2020-07-23 16:50:39.871206 | container |  import koji
2020-07-23 16:50:39.871212 | container |
2020-07-23 16:50:39.871218 | container |  from ..base import ToddlerBase
2020-07-23 16:50:39.872211 | container | would reformat toddlers/plugins/pdc_modules.py
2020-07-23 16:50:39.888394 | container | Oh no! 💥 💔 💥
2020-07-23 16:50:39.888474 | container | 1 file would be reformatted, 35 files would be left unchanged.
2020-07-23 16:50:39.909791 | container | ERROR: InvocationError for command /root/src/pagure.io/fedora-infra/toddlers/.tox/black/bin/black --check --diff . (exited with code 1)

And this is the output from flake8:

2020-07-23 16:50:42.534495 | container | [493] /root/src/pagure.io/fedora-infra/toddlers$ /root/src/pagure.io/fedora-infra/toddlers/.tox/flake8/bin/flake8 .
2020-07-23 16:50:43.324649 | container | ./toddlers/plugins/pdc_import_compose.py:15:1: F401 'pdc_client' imported but unused
2020-07-23 16:50:43.324700 | container | import pdc_client
2020-07-23 16:50:43.324714 | container | ^
2020-07-23 16:50:43.324721 | container | ./toddlers/plugins/pdc_modules.py:17:1: E402 module level import not at top of file
2020-07-23 16:50:43.324725 | container | from gi.repository import Modulemd
2020-07-23 16:50:43.324730 | container | ^
2020-07-23 16:50:43.324735 | container | ./toddlers/plugins/pdc_modules.py:18:1: E402 module level import not at top of file
2020-07-23 16:50:43.324739 | container | import koji
2020-07-23 16:50:43.324744 | container | ^
2020-07-23 16:50:43.324768 | container | ./toddlers/plugins/pdc_modules.py:20:1: E402 module level import not at top of file
2020-07-23 16:50:43.324787 | container | from ..base import ToddlerBase
2020-07-23 16:50:43.324798 | container | ^
2020-07-23 16:50:43.324806 | container | ./toddlers/plugins/pdc_modules.py:21:1: E402 module level import not at top of file
2020-07-23 16:50:43.324810 | container | from ..utils.pdc import pdc_client_for_config
2020-07-23 16:50:43.324814 | container | ^
2020-07-23 16:50:43.324818 | container | ./toddlers/plugins/pdc_modules.py:22:1: E402 module level import not at top of file
2020-07-23 16:50:43.324821 | container | from ..utils.requests import make_session
2020-07-23 16:50:43.324825 | container | ^
2020-07-23 16:50:43.362136 | container | ERROR: InvocationError for command /root/src/pagure.io/fedora-infra/toddlers/.tox/flake8/bin/flake8 . (exited with code 1)

So I think my solution is better, even if it's not in sorted order.

Hmm, sometimes black is just too damn stupid for being so uncompromising… Theoretically, we could move all the lines related to gi imports into their own module and import from there, but as it only affects a single file it's better to make an exception regarding the sort order so we only need one # noqa: E402.

Just for shits and giggles, I found a way to appease both black and flake8 without moving the lines to the bottom of the import block:

import beanbag
import gi

if f"{gi.require_version('Modulemd', '1.0')}":
    from gi.repository import Modulemd
import koji  # noqa: I202

from ..base import ToddlerBase

The only problem is, it's ugly as sin and probably scarier than anything else @pingou has seen this week. :joy:

One other thing that struck me, we probably could take this as an opportunity to port the code to modulemd-2.0, I'm sure @sgallagh would be happy to lay the old API to rest eventually.

Note: this isn't a full review, I didn't get around to all the rest. I'll drop myself as assignee because I'm out tomorrow and don't want to block the review.

Metadata Update from @nphilipp:
- Request assignee reset

3 years ago

rebased onto a0d8a183361e53fc1d4f9fc14005958b65ed1da3

3 years ago

Hm, we already have a koji_url in the default, could we just rely on that one? (And append the kojihub in the code if needed?)

Looking at the amqp_topics, it seems this toddler listens for MBS messages, not playtime's ones.

Ok, we now from above that _get_pdc_api() can raise an exception, which will make this toddler loop. Are we ok with this?

In this case, since there is only a "process a single state change" and not a "catch up history", do we need the function here? Shouldn't we just merge both?

This can raise an exception, do we want to catch it, or are we fine looping until it works?

Build failed.

  • tox : FAILURE in 7m 12s

Hm, we already have a koji_url in the default, could we just rely on that one? (And append the kojihub in the code if needed?)

Didn't noticed there is already one in the config, how can I get to it?

Left over? :)

Not this line, probably the one bellow could be removed.

Looking at the amqp_topics, it seems this toddler listens for MBS messages, not playtime's ones.

I thought, that this is correct. That the trigger message is only needed if you need to do something more than just consume the message.

Ok, we now from above that _get_pdc_api() can raise an exception, which will make this toddler loop. Are we ok with this?

Not sure, I copied this from the original modules.py code and if I understand it correctly, it should happen only if there is some issue on the PDC side.

In this case, since there is only a "process a single state change" and not a "catch up history", do we need the function here? Shouldn't we just merge both?

We could definitely merge it, I just used the same code structure as I saw in retired_packages. I'm not sure how the "catch up history" works.

rebased onto 2538e5cf36f506ebd6ff36dee297e8ab929ccbc1

3 years ago

Build succeeded.

  • tox : SUCCESS in 4m 32s

Hm, we already have a koji_url in the default, could we just rely on that one? (And append the kojihub in the code if needed?)

Didn't noticed there is already one in the config, how can I get to it?

Everything that is in the default config is available in the toddler's config.
The default and the toddler's specific dicts are merged and the result is passed
onto the toddler. So you can assume everything that is in the default dict is
available to your toddler.

Left over? :)

Not this line, probably the one bellow could be removed.

Looking at the amqp_topics, it seems this toddler listens for MBS messages, not playtime's ones.

I thought, that this is correct. That the trigger message is only needed if you need to do something more than just consume the message.

It is correct, but the docstring there says otherwise ;-)

Ok, we now from above that _get_pdc_api() can raise an exception, which will make this toddler loop. Are we ok with this?

Not sure, I copied this from the original modules.py code and if I understand it correctly, it should happen only if there is some issue on the PDC side.

In this case, since there is only a "process a single state change" and not a "catch up history", do we need the function here? Shouldn't we just merge both?

We need to be careful with exceptions and try to have as little as possible, as
otherwise it send toddlers into an infinite loop.

We could definitely merge it, I just used the same code structure as I saw in retired_packages. I'm not sure how the "catch up history" works.

So in the pdc-handler there were three main functions as far as I saw:
- handle: basically handle an incoming message
- audit: used to check if the source of info and PDC are in sync
- initialize: load PDC from all the old data it can find

If you look at retirement.py in pdc-updater the docstring may help you.

In toddlers, I have converted handle to process and often used two triggers, one
from the main source of info (koji, mbs, dist-git...) and one for playtime.
Then I was checking which of the two triggers triggered the toddler. If it's the
main source of info, then it's the "handle" route, single action, if it's
playtime, then it's the "initialize" route.

Does that make sense?

Everything that is in the default config is available in the toddler's config.
The default and the toddler's specific dicts are merged and the result is passed
onto the toddler. So you can assume everything that is in the default dict is
available to your toddler.

I will remove it from the consumer config then :-)

It is correct, but the docstring there says otherwise ;-)

I copied the docstring from the other PDC plugin and than just did a little change, I will rewrite it, so it's correct.

We need to be careful with exceptions and try to have as little as possible, as
otherwise it send toddlers into an infinite loop.

I could just Nack the message instead of raising exception, because I can't continue anyway. Do you think it's a correct approach?

So in the pdc-handler there were three main functions as far as I saw:
- handle: basically handle an incoming message
- audit: used to check if the source of info and PDC are in sync
- initialize: load PDC from all the old data it can find
If you look at retirement.py in pdc-updater the docstring may help you.
In toddlers, I have converted handle to process and often used two triggers, one
from the main source of info (koji, mbs, dist-git...) and one for playtime.
Then I was checking which of the two triggers triggered the toddler. If it's the
main source of info, then it's the "handle" route, single action, if it's
playtime, then it's the "initialize" route.
Does that make sense?

Yes, the modules.py didn't have any initialization neither audit, there is just pass function in those methods. So it will be best to just go with the handle route. I will merge the _process_single_state_change and process to one method.

This can raise an exception, do we want to catch it, or are we fine looping until it works?

Do you mean the requests_session? I could Nack the message if it's raised, so we know that we are still interested in the message, but can't handle it at this time.

One other thing that struck me, we probably could take this as an opportunity to port the code to modulemd-2.0, I'm sure @sgallagh would be happy to lay the old API to rest eventually.

I like this question, I would like to confirm that the data in PDC is actually being used though (maybe @sgallagh would know?). We're porting it as is because it is running currently in pdc-updater (or was until the colo move), investing more time to port it to the new modulemd code will need more time and we'll want to be sure that this is used/useful.

@zlopez I don't think you need to catch an exception and nack the message if we're ok with looping until it fixes itself, the nacking happens in the runner, so in those case I think we're good as is.

I just wanted us to be sure that we are ok with the "loop until fix" behavior on those cases :)

@pingou If we encounter error in this case and the exception is raised, the consumer couldn't do much anyway, so it should be okay to "loop until fixed".

1 new commit added

  • Refactor pdc_modules
3 years ago

Build succeeded.

  • tox : SUCCESS in 4m 30s

One other thing that struck me, we probably could take this as an opportunity to port the code to modulemd-2.0, I'm sure @sgallagh would be happy to lay the old API to rest eventually.

I like this question, I would like to confirm that the data in PDC is actually being used though (maybe @sgallagh would know?). We're porting it as is because it is running currently in pdc-updater (or was until the colo move), investing more time to port it to the new modulemd code will need more time and we'll want to be sure that this is used/useful.

I don't know. @psabata or @jkaluza might, however.

Don't use normal string formatting when logging, but this (mind the comma instead of '%' between the string and `pdc_api'):

    _log.debug("Using the %s PDC API.", pdc_api)

This way, string interpolation will only happen if we log at a level of DEBUG , i.e. not during normal operations.

I'd use a dict literal ({...}) here, 1) because it's easier to visually identify it as one and 2) because using dict(...) instead is almost 4 times slower. I.e.:

    for rpm in koji_rpms:
        data = {
            "name": rpm["name"],
            "version": rpm["version"],
            "release": rpm["release"],
            "epoch": rpm["epoch"] or 0,
            "arch": rpm["arch"],
            "srpm_name": rpm["srpm_name"],
        }

rebased onto 4e5a3d2

3 years ago

Build failed.

  • tox : FAILURE in 5m 57s

4 new commits added

  • Refactor pdc_modules
  • Fix import error
  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Build succeeded.

  • tox : SUCCESS in 5m 20s

Metadata Update from @nphilipp:
- Request assigned

3 years ago

4 new commits added

  • Refactor pdc_modules
  • Fix import error
  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Build succeeded.

  • tox : SUCCESS in 5m 27s

4 new commits added

  • Refactor pdc_modules
  • Fix import error
  • Update zuul
  • Add the pdc_modules toddler
3 years ago

Looks good to me, thanks!

Build succeeded.

  • tox : SUCCESS in 7m 34s

Pull-Request has been merged by nphilipp

3 years ago