From f98892af9a255da2ad25d8fde076909b9bec7202 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi Date: Jun 14 2017 12:09:27 +0000 Subject: Find docker images to rebuild which contains a RPM signed in Brew This patch is for finding docker images to rebuild when included RPM is signed in Brew. Signed-off-by: Chenxiong Qi --- diff --git a/conf/configrh.py b/conf/configrh.py index 44fb809..8da3dcb 100644 --- a/conf/configrh.py +++ b/conf/configrh.py @@ -25,8 +25,11 @@ class BaseConfiguration(config.BaseConfiguration): ] HANDLERS = [ + 'freshmaker.handlers.brew:BrewSignRPMHanlder', ] + KOJI_PROFILE = 'brew' + # LightBlue server URL, e.g. http://localhost/ LIGHTBLUE_SERVER_URL = '' # replace with default server url LIGHTBLUE_VERIFY_SSL = True diff --git a/freshmaker/errata.py b/freshmaker/errata.py index 671647b..c77d22c 100644 --- a/freshmaker/errata.py +++ b/freshmaker/errata.py @@ -104,7 +104,7 @@ class Errata(object): :return: True if all builds in advisory are signed. :rtype: bool """ - builds_per_product = self._errata_rest_get( + builds_per_product = self._errata_http_get( "advisory/%s/builds.json" % str(errata_id)) # Store NVRs of all builds in advisory to nvrs set. diff --git a/freshmaker/handlers/brew/__init__.py b/freshmaker/handlers/brew/__init__.py new file mode 100644 index 0000000..fe210f2 --- /dev/null +++ b/freshmaker/handlers/brew/__init__.py @@ -0,0 +1,22 @@ +# -*- 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 .sign_rpm import BrewSignRPMHanlder # noqa diff --git a/freshmaker/handlers/brew/sign_rpm.py b/freshmaker/handlers/brew/sign_rpm.py new file mode 100644 index 0000000..dbd0c44 --- /dev/null +++ b/freshmaker/handlers/brew/sign_rpm.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2016 Red Hat, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Written by Chenxiong Qi + +from itertools import chain + +from freshmaker import conf +from freshmaker import log +from freshmaker.events import BrewSignRPMEvent +from freshmaker.handlers import BaseHandler +from freshmaker.kojiservice import koji_service +from freshmaker.lightblue import LightBlue +from freshmaker.pulp import Pulp +from freshmaker.errata import Errata + + +class BrewSignRPMHanlder(BaseHandler): + """Rebuild docker images when a RPM is signed in Brew""" + + name = 'BrewSignRPMHandler' + + def can_handle(self, event): + return isinstance(event, BrewSignRPMEvent) + + def handle(self, event): + """Rebuild docker images which contains this signed RPM + + Before rebuilding docker images, freshmaker has to find which docker + images includes the signed RPM. As of writing this feature, this + information is stored in LightBlue, and need to use content_sets to + find out those images. + + There are several external services taking part in the process of + rebuilding docker images. + + * Errata Tool: get which advisories contains the signed RPM, and Pulp + repositories the signed RPM will end up eventually when shipped. + * Pulp: query content set with repositories got from Errata Tool. + * LightBlue: this is where to query docker images that contains RPMs + from those content sets. + """ + + images = self._find_images_to_rebuild(event) + + if not images: + log.info('Not find docker images to rebuild.') + return [] + + log.info('Found docker images to rebuild: %s', images) + + # TODO: build yum repo to contain that signed RPM and start to rebuild + + return [] + + def _find_images_to_rebuild(self, event): + # When get a signed RPM, first step is to find out advisories + # containing that RPM and has to ensure all builds are signed. + errata = Errata(conf.errata_tool_server_url) + advisories = errata.advisories_from_event(event) + + if not all((errata.builds_signed(advisory.errata_id) + for advisory in advisories)): + log.info('Not all builds in %s are signed. Do not rebuild any ' + 'docker image until signed.', advisories) + return [] + + # Use the advisories to find out Pulp repository IDs from Errata Tool + # and furthermore get content_sets from Pulp where signed RPM will end + # up eventually when advisories are shipped. + pulp_repo_ids = list(set(chain( + *[errata.get_pulp_repository_ids(advisory.errata_id) + for advisory in advisories] + ))) + + pulp = Pulp(server_url=conf.pulp_server_url, + username=conf.pulp_username, + password=conf.pulp_password) + content_sets = pulp.get_content_set_by_repo_ids(pulp_repo_ids) + + log.info('RPM will end up within content sets %s', content_sets) + + # Query images from LightBlue by signed RPM's srpm name and found + # content sets + lb = LightBlue(server_url=conf.lightblue_server_url, + cert=conf.lightblue_certificate, + private_key=conf.lightblue_private_key) + + srpm_name = self._find_build_srpm_name(event.nvr) + return lb.find_images_with_package_from_content_set(srpm_name, + content_sets) + + def _find_build_srpm_name(self, build_nvr): + """Find srpm name from a build""" + with koji_service(conf.koji_profile, log) as session: + rpm_infos = session.get_build_rpms(build_nvr, arches='src') + if not rpm_infos: + raise ValueError( + 'Build {} does not have a SRPM, although this should not ' + 'happen in practice.'.format(build_nvr)) + return rpm_infos[0]['name'] diff --git a/freshmaker/kojiservice.py b/freshmaker/kojiservice.py index edce528..a08ab84 100644 --- a/freshmaker/kojiservice.py +++ b/freshmaker/kojiservice.py @@ -95,9 +95,10 @@ class KojiService(object): return task_id - def get_build_rpms(self, build_nvr): + def get_build_rpms(self, build_nvr, arches=None): build_info = self.session.getBuild(build_nvr) - return self.session.listRPMs(buildID=build_info['id']) + return self.session.listRPMs(buildID=build_info['id'], + arches=arches) @contextlib.contextmanager diff --git a/freshmaker/lightblue.py b/freshmaker/lightblue.py index 1b26187..3208ffe 100644 --- a/freshmaker/lightblue.py +++ b/freshmaker/lightblue.py @@ -127,7 +127,7 @@ class LightBlue(object): will be used on each entity. """ self.server_url = server_url.rstrip('/') - self.api_root = '{}/rest/data'.format(server_url) + self.api_root = '{}/rest/data'.format(self.server_url) if verify_ssl is None: self.verify_ssl = True else: @@ -375,7 +375,6 @@ class LightBlue(object): break for rpm in image["parsed_data"]["rpm_manifest"]: - print(rpm) if rpm["srpm_name"] == srpm_name: srpm_nevra = rpm['srpm_nevra'] break @@ -385,5 +384,6 @@ class LightBlue(object): reponame = reponame.replace("/plain/Dockerfile", "") commits.append({"repository": reponame, "commit": commit, - "srpm_nevra": srpm_nevra}) + "srpm_nevra": srpm_nevra, + "brew": image["brew"]}) return commits diff --git a/freshmaker/pulp.py b/freshmaker/pulp.py index eeb703c..027bdfb 100644 --- a/freshmaker/pulp.py +++ b/freshmaker/pulp.py @@ -21,6 +21,7 @@ # # Written by Chenxiong Qi +import json import requests @@ -33,12 +34,29 @@ class Pulp(object): self.server_url = server_url self.rest_api_root = '{0}/pulp/api/v2/'.format(self.server_url.rstrip('/')) - def _rest_get(self, endpoint): - r = requests.get('{0}{1}'.format(self.rest_api_root, endpoint.lstrip('/')), - auth=(self.username, self.password)) + def _rest_post(self, endpoint, post_data): + r = requests.post( + '{0}{1}'.format(self.rest_api_root, endpoint.lstrip('/')), + post_data, + auth=(self.username, self.password)) r.raise_for_status() return r.json() - def get_content_set_by_repo_id(self, repo_id): - data = self._rest_get('repositories/{0}/'.format(repo_id)) - return data['notes']['content_set'] + def get_content_set_by_repo_ids(self, repo_ids): + """Get content_sets by repository IDs + + :param list repo_ids: list of repository IDs. + :return: list of names of content_sets. + :rtype: list + """ + query_data = { + 'criteria': { + 'filters': { + 'id': {'$in': repo_ids}, + }, + 'fields': ['notes.content_set'], + } + } + repos = self._rest_post('repositories/search/', json.dumps(query_data)) + return [repo['notes']['content_set'] for repo in repos + if 'content_set' in repo['notes']] diff --git a/tests/test_brew_sign_rpm_handler.py b/tests/test_brew_sign_rpm_handler.py new file mode 100644 index 0000000..7dfd728 --- /dev/null +++ b/tests/test_brew_sign_rpm_handler.py @@ -0,0 +1,80 @@ +# -*- 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 + +import six +import pytest +import unittest + +from mock import patch + +from freshmaker.handlers.brew.sign_rpm import BrewSignRPMHanlder + + +@pytest.mark.skipif(six.PY3, reason='koji does not work in Python 3') +class TestFindBuildSrpmName(unittest.TestCase): + """Test BrewSignRPMHanlder._find_build_srpm_name""" + + @patch('koji.ClientSession') + def test_find_srpm_name(self, ClientSession): + session = ClientSession.return_value + session.getBuild.return_value = { + 'build_id': 439408, + 'id': 439408, + 'name': 'bind-dyndb-ldap', + 'nvr': 'bind-dyndb-ldap-2.3-8.el6', + } + session.listRPMs.return_value = [{ + 'arch': 'src', + 'name': 'bind-dyndb-ldap', + 'nvr': 'bind-dyndb-ldap-2.3-8.el6', + }] + + handler = BrewSignRPMHanlder() + srpm_name = handler._find_build_srpm_name('bind-dyndb-ldap-2.3-8.el6') + + session.getBuild.assert_called_once_with('bind-dyndb-ldap-2.3-8.el6') + session.listRPMs.assert_called_once_with(buildID=439408, arches='src') + self.assertEqual('bind-dyndb-ldap', srpm_name) + + @patch('koji.ClientSession') + def test_error_if_no_srpm_in_build(self, ClientSession): + session = ClientSession.return_value + session.getBuild.return_value = { + 'build_id': 439408, + 'id': 439408, + 'name': 'bind-dyndb-ldap', + 'nvr': 'bind-dyndb-ldap-2.3-8.el6', + } + session.listRPMs.return_value = [] + + handler = BrewSignRPMHanlder() + + self.assertRaisesRegexp( + ValueError, + 'Build bind-dyndb-ldap-2.3-8.el6 does not have a SRPM', + handler._find_build_srpm_name, + 'bind-dyndb-ldap-2.3-8.el6', + ) + + session.getBuild.assert_called_once_with('bind-dyndb-ldap-2.3-8.el6') + session.listRPMs.assert_called_once_with(buildID=439408, arches='src') diff --git a/tests/test_errata.py b/tests/test_errata.py index d08dfe1..e261d8b 100644 --- a/tests/test_errata.py +++ b/tests/test_errata.py @@ -28,12 +28,14 @@ from freshmaker.errata import Errata from freshmaker.events import BrewSignRPMEvent, GitRPMSpecChangeEvent -class MockedErrataRESTAPI(object): +class MockedErrataAPI(object): """ Class mocking methods accessing Errata API in Errata class. """ - def __init__(self, errata_rest_get): + def __init__(self, errata_rest_get, errata_http_get=None): errata_rest_get.side_effect = (self.errata_rest_get) + if errata_http_get: + errata_http_get.side_effect = self.errata_http_get self.builds_json = { "PRODUCT1": [ @@ -67,12 +69,14 @@ class MockedErrataRESTAPI(object): "rpms_signed": True} def errata_rest_get(self, endpoint): - if endpoint.endswith("builds.json"): - return self.builds_json - elif endpoint.find("build/") != -1: + if endpoint.find("build/") != -1: nvr = endpoint.split("/")[-1] return self.builds[nvr] + def errata_http_get(self, endpoint): + if endpoint.endswith("builds.json"): + return self.builds_json + class TestErrata(unittest.TestCase): def setUp(self): @@ -80,7 +84,7 @@ class TestErrata(unittest.TestCase): @patch.object(Errata, "_errata_rest_get") def test_advisories_from_event(self, errata_rest_get): - MockedErrataRESTAPI(errata_rest_get) + MockedErrataAPI(errata_rest_get) event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") advisories = self.errata.advisories_from_event(event) self.assertEqual(len(advisories), 1) @@ -88,7 +92,7 @@ class TestErrata(unittest.TestCase): @patch.object(Errata, "_errata_rest_get") def test_advisories_from_event_missing_all_errata(self, errata_rest_get): - mocked_errata = MockedErrataRESTAPI(errata_rest_get) + mocked_errata = MockedErrataAPI(errata_rest_get) del mocked_errata.builds["libntirpc-1.4.3-4.el7rhgs"]["all_errata"] event = BrewSignRPMEvent("msgid", "libntirpc-1.4.3-4.el7rhgs") @@ -101,19 +105,22 @@ class TestErrata(unittest.TestCase): self.errata.advisories_from_event(event) @patch.object(Errata, "_errata_rest_get") - def test_builds_signed_all_signed(self, errata_rest_get): - MockedErrataRESTAPI(errata_rest_get) + @patch.object(Errata, "_errata_http_get") + def test_builds_signed_all_signed(self, errata_http_get, errata_rest_get): + MockedErrataAPI(errata_rest_get, errata_http_get) self.assertTrue(self.errata.builds_signed(28484)) @patch.object(Errata, "_errata_rest_get") - def test_builds_signed_some_unsigned(self, errata_rest_get): - mocked_errata = MockedErrataRESTAPI(errata_rest_get) + @patch.object(Errata, "_errata_http_get") + def test_builds_signed_some_unsigned(self, errata_http_get, errata_rest_get): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) mocked_errata.builds["libntirpc-1.4.3-4.el7rhgs"]["rpms_signed"] = False self.assertFalse(self.errata.builds_signed(28484)) @patch.object(Errata, "_errata_rest_get") - def test_builds_signed_missing_data(self, errata_rest_get): - mocked_errata = MockedErrataRESTAPI(errata_rest_get) + @patch.object(Errata, "_errata_http_get") + def test_builds_signed_missing_data(self, errata_http_get, errata_rest_get): + mocked_errata = MockedErrataAPI(errata_rest_get, errata_http_get) mocked_errata.builds["libntirpc-1.4.3-4.el7rhgs"] = {} self.assertFalse(self.errata.builds_signed(28484)) diff --git a/tests/test_lightblue.py b/tests/test_lightblue.py index 1c2b85e..4653397 100644 --- a/tests/test_lightblue.py +++ b/tests/test_lightblue.py @@ -519,12 +519,22 @@ class TestQueryEntityFromLightBlue(unittest.TestCase): { "repository": "rpms/repo-1", "commit": "commit_hash1", - "srpm_nevra": "openssl-0:1.2.3-1.src" + "srpm_nevra": "openssl-0:1.2.3-1.src", + "brew": { + "completion_date": u"20170421T04:27:51.000-0400", + "build": "package-name-1-4-12.10", + "package": "package-name-1" + } }, { "repository": "ns/repo-2", "commit": "commit_hash2", - "srpm_nevra": "openssl-1:1.2.3-1.src" + "srpm_nevra": "openssl-1:1.2.3-1.src", + "brew": { + "completion_date": u"20170421T04:27:51.000-0400", + "build": "package-name-2-4-12.10", + "package": "package-name-2" + } } ]) diff --git a/tests/test_pulp.py b/tests/test_pulp.py index e258278..b5e1b13 100644 --- a/tests/test_pulp.py +++ b/tests/test_pulp.py @@ -21,6 +21,7 @@ # # Written by Chenxiong Qi +import json import unittest from mock import patch @@ -36,34 +37,123 @@ class TestPulp(unittest.TestCase): self.username = 'qa' self.password = 'qa' - @patch('freshmaker.pulp.requests.get') - def test_query_content_set_by_repo_id(self, get): - get.return_value.json.return_value = { - 'id': 'rhel-7-desktop-debug-rpms__7Client__x86_64', - '_ns': 'repos', - 'content_unit_counts': { - 'erratum': 1420, - 'rpm': 4773 + @patch('freshmaker.pulp.requests.post') + def test_query_content_set_by_repo_ids(self, post): + post.return_value.json.return_value = [ + { + '_href': '/pulp/api/v2/repositories/rhel-7-workstation-rpms__7Workstation__x86_64/', + '_id': + { + '$oid': '53853a247bc9f61b85909cfe' + }, + 'id': 'rhel-7-workstation-rpms__7Workstation__x86_64', + 'notes': + { + 'content_set': 'rhel-7-workstation-rpms', + }, }, - '_id': { - '$oid': '5384ee687bc9f619942a8b47' + { + '_href': '/pulp/api/v2/repositories/rhel-7-hpc-node-rpms__7ComputeNode__x86_64/', + '_id': { + '$oid': '5384ee7c7bc9f619942a8f89', + }, + 'id': 'rhel-7-hpc-node-rpms__7ComputeNode__x86_64', + 'notes': { + 'content_set': 'rhel-7-hpc-node-rpms' + }, }, - 'last_unit_added': '2017-06-02T15:01:06Z', - 'notes': { - 'arch': 'x86_64', - 'content_set': 'rhel-7-desktop-debug-rpms', - 'platform_full_version': '7', + { + '_href': '/pulp/api/v2/repositories/rhel-7-desktop-rpms__7Client__x86_64/', + '_id': { + '$oid': '5384ee6a7bc9f619942a8bca', + }, + 'id': 'rhel-7-desktop-rpms__7Client__x86_64', + 'notes': { + 'content_set': 'rhel-7-desktop-rpms', + } + } + ] + + pulp = Pulp(self.server_url, username=self.username, password=self.password) + repo_ids = [ + 'rhel-7-hpc-node-rpms__7ComputeNode__x86_64', + 'rhel-7-workstation-rpms__7Workstation__x86_64', + 'rhel-7-desktop-rpms__7Client__x86_64', + ] + content_sets = pulp.get_content_set_by_repo_ids(repo_ids) + + post.assert_called_once_with( + '{}pulp/api/v2/repositories/search/'.format(self.server_url), + json.dumps({ + 'criteria': { + 'filters': { + 'id': {'$in': repo_ids}, + }, + 'fields': ['notes.content_set'], + } + }), + auth=(self.username, self.password)) + + self.assertEqual( + ['rhel-7-workstation-rpms', + 'rhel-7-hpc-node-rpms', + 'rhel-7-desktop-rpms'], + content_sets) + + @patch('freshmaker.pulp.requests.post') + def test_get_content_sets_by_ignoring_nonexisting_ones(self, post): + post.return_value.json.return_value = [ + { + '_href': '/pulp/api/v2/repositories/rhel-7-workstation-rpms__7Workstation__x86_64/', + '_id': + { + '$oid': '53853a247bc9f61b85909cfe' + }, + 'id': 'rhel-7-workstation-rpms__7Workstation__x86_64', + 'notes': + { + 'content_set': 'rhel-7-workstation-rpms', + }, + }, + { + '_href': '/pulp/api/v2/repositories/rhel-7-hpc-node-rpms__7ComputeNode__x86_64/', + '_id': { + '$oid': '5384ee7c7bc9f619942a8f89', + }, + 'id': 'rhel-7-hpc-node-rpms__7ComputeNode__x86_64', + 'notes': {}, }, - 'display_name': 'rhel-7-desktop-debug-rpms__7Client__x86_64', - 'description': None, - } + { + '_href': '/pulp/api/v2/repositories/rhel-7-desktop-rpms__7Client__x86_64/', + '_id': { + '$oid': '5384ee6a7bc9f619942a8bca', + }, + 'id': 'rhel-7-desktop-rpms__7Client__x86_64', + 'notes': { + 'content_set': 'rhel-7-desktop-rpms', + } + } + ] pulp = Pulp(self.server_url, username=self.username, password=self.password) - repo_id = 'rhel-7-desktop-debug-rpms__7Client__x86_64' - content_set = pulp.get_content_set_by_repo_id(repo_id) + repo_ids = [ + 'rhel-7-hpc-node-rpms__7ComputeNode__x86_64', + 'rhel-7-workstation-rpms__7Workstation__x86_64', + 'rhel-7-desktop-rpms__7Client__x86_64', + ] + content_sets = pulp.get_content_set_by_repo_ids(repo_ids) - get.assert_called_once_with( - '{}pulp/api/v2/repositories/{}/'.format(self.server_url, repo_id), + post.assert_called_once_with( + '{}pulp/api/v2/repositories/search/'.format(self.server_url), + json.dumps({ + 'criteria': { + 'filters': { + 'id': {'$in': repo_ids}, + }, + 'fields': ['notes.content_set'], + } + }), auth=(self.username, self.password)) - self.assertEqual('rhel-7-desktop-debug-rpms', content_set) + self.assertEqual(['rhel-7-workstation-rpms', 'rhel-7-desktop-rpms'], + content_sets)