diff --git a/Makefile b/Makefile index ff0920c70ee..f10fd98ea82 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,7 @@ PYTEST_XDIST_OPTS=$(PYTEST_OPTS) -n $(PYTEST_XDIST_NUMPROCESSES) ROBOTTELO_TESTS_PATH=tests/robottelo/ TESTIMONY_OPTIONS=--config testimony.yaml + # Commands -------------------------------------------------------------------- help: diff --git a/conf/capsule.yaml.template b/conf/capsule.yaml.template index 9ef9e48a2c2..b618bf57ea9 100644 --- a/conf/capsule.yaml.template +++ b/conf/capsule.yaml.template @@ -1,4 +1,6 @@ CAPSULE: + # Capsule hostname for N-minus testing + HOSTNAME: VERSION: # The full release version (6.9.2) RELEASE: # populate with capsule version diff --git a/conf/dynaconf_hooks.py b/conf/dynaconf_hooks.py index 6d09d6e5bec..559ef369a60 100644 --- a/conf/dynaconf_hooks.py +++ b/conf/dynaconf_hooks.py @@ -85,9 +85,9 @@ def get_ohsnap_repos(settings): settings, repo='capsule', product='capsule', - release=settings.server.version.release, - os_release=settings.server.version.rhel_version, - snap=settings.server.version.snap, + release=settings.capsule.version.release, + os_release=settings.capsule.version.release, + snap=settings.capsule.version.snap, ) data['SATELLITE_REPO'] = get_ohsnap_repo_url( diff --git a/conftest.py b/conftest.py index 42682d76249..7645728a06c 100644 --- a/conftest.py +++ b/conftest.py @@ -21,6 +21,7 @@ 'pytest_plugins.requirements.update_requirements', 'pytest_plugins.sanity_plugin', 'pytest_plugins.video_cleanup', + 'pytest_plugins.capsule_n-minus', # Fixtures 'pytest_fixtures.core.broker', 'pytest_fixtures.core.sat_cap_factory', diff --git a/pytest_fixtures/core/sat_cap_factory.py b/pytest_fixtures/core/sat_cap_factory.py index c63a70e4d19..0c23b1ee4d2 100644 --- a/pytest_fixtures/core/sat_cap_factory.py +++ b/pytest_fixtures/core/sat_cap_factory.py @@ -1,4 +1,5 @@ from contextlib import contextmanager +from functools import lru_cache from broker import Broker from packaging.version import Version @@ -37,13 +38,29 @@ def _target_satellite_host(request, satellite_factory): yield +@lru_cache +def cached_capsule_cdn_register(hostname=None): + cap = Capsule.get_host_by_hostname(hostname=hostname) + cap.enable_capsule_downstream_repos() + + @contextmanager def _target_capsule_host(request, capsule_factory): - if 'sanity' not in request.config.option.markexpr: + if 'sanity' not in request.config.option.markexpr and not request.config.option.n_minus: new_cap = capsule_factory() yield new_cap new_cap.teardown() Broker(hosts=[new_cap]).checkin() + elif request.config.option.n_minus: + if not settings.capsule.hostname: + hosts = Capsule.get_hosts_from_inventory(filter="'cap' in @inv.name") + settings.capsule.hostname = hosts[0].hostname + cap = hosts[0] + else: + cap = Capsule.get_host_by_hostname(settings.capsule.hostname) + # Capsule needs RHEL contents for some tests + cached_capsule_cdn_register(hostname=settings.capsule.hostname) + yield cap else: yield @@ -162,9 +179,10 @@ def session_capsule_host(request, capsule_factory): @pytest.fixture -def capsule_configured(capsule_host, target_sat): +def capsule_configured(request, capsule_host, target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" - capsule_host.capsule_setup(sat_host=target_sat) + if not request.config.option.n_minus: + capsule_host.capsule_setup(sat_host=target_sat) return capsule_host @@ -176,21 +194,23 @@ def large_capsule_configured(large_capsule_host, target_sat): @pytest.fixture(scope='module') -def module_capsule_configured(module_capsule_host, module_target_sat): +def module_capsule_configured(request, module_capsule_host, module_target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" - module_capsule_host.capsule_setup(sat_host=module_target_sat) + if not request.config.option.n_minus: + module_capsule_host.capsule_setup(sat_host=module_target_sat) return module_capsule_host @pytest.fixture(scope='session') -def session_capsule_configured(session_capsule_host, session_target_sat): +def session_capsule_configured(request, session_capsule_host, session_target_sat): """Configure the capsule instance with the satellite from settings.server.hostname""" - session_capsule_host.capsule_setup(sat_host=session_target_sat) + if not request.config.option.n_minus: + session_capsule_host.capsule_setup(sat_host=session_target_sat) return session_capsule_host @pytest.fixture(scope='module') -def module_capsule_configured_mqtt(module_capsule_configured): +def module_capsule_configured_mqtt(request, module_capsule_configured): """Configure the capsule instance with the satellite from settings.server.hostname, enable MQTT broker""" module_capsule_configured.set_rex_script_mode_provider('pull-mqtt') @@ -201,7 +221,9 @@ def module_capsule_configured_mqtt(module_capsule_configured): result = module_capsule_configured.execute('firewall-cmd --permanent --add-port="1883/tcp"') assert result.status == 0, 'Failed to open mqtt port on capsule' module_capsule_configured.execute('firewall-cmd --reload') - return module_capsule_configured + yield module_capsule_configured + if request.config.option.n_minus: + raise TypeError('The teardown is missed for MQTT configuration undo for nminus testing') @pytest.fixture(scope='module') diff --git a/pytest_plugins/capsule_n-minus.py b/pytest_plugins/capsule_n-minus.py new file mode 100644 index 00000000000..e3b0c0de16a --- /dev/null +++ b/pytest_plugins/capsule_n-minus.py @@ -0,0 +1,55 @@ +# Collection of Capsule Factory fixture tests +# No destructive tests +# Adjust capsule host and capsule_configured host behavior for n_minus testing +# Calculate capsule hostname from inventory just as we do in xDist.py +from robottelo.config import settings +from robottelo.hosts import Capsule + + +def pytest_addoption(parser): + """Add options for pytest to collect tests based on fixtures its using""" + help_text = ''' + Collects tests based on capsule fixtures used by tests and uncollect destructive tests + + Usage: --n-minus + + example: pytest --n-minus tests/foreman + ''' + parser.addoption("--n-minus", action='store_true', default=False, help=help_text) + + +def pytest_collection_modifyitems(items, config): + + if not config.getoption('n_minus', False): + return + + selected = [] + deselected = [] + + for item in items: + is_destructive = item.get_closest_marker('destructive') + # Deselect Destructive tests and tests without capsule_factory fixture + if 'capsule_factory' not in item.fixturenames or is_destructive: + deselected.append(item) + continue + # Ignoring all puppet tests as they are destructive in nature + # and needs its own satellite for verification + if 'session_puppet_enabled_sat' in item.fixturenames: + deselected.append(item) + continue + # Ignoring all satellite maintain tests as they are destructive in nature + # Also dont need them in nminus testing as its not integration testing + if 'sat_maintain' in item.fixturenames and 'satellite' in item.callspec.params.values(): + deselected.append(item) + continue + selected.append(item) + + config.hook.pytest_deselected(items=deselected) + items[:] = selected + + +def pytest_sessionfinish(session, exitstatus): + # Unregister the capsule from CDN after all tests + if session.config.option.n_minus and not session.config.option.collectonly: + caps = Capsule(hostname=settings.capsule.hostname) + caps.unregister() diff --git a/robottelo/exceptions.py b/robottelo/exceptions.py index 83022dfcd6e..a6100564873 100644 --- a/robottelo/exceptions.py +++ b/robottelo/exceptions.py @@ -75,6 +75,12 @@ class CLIError(Exception): """Indicates that a CLI command could not be run.""" +class CapsuleHostError(Exception): + """Indicates error in capsule configuration etc""" + + pass + + class CLIBaseError(Exception): """Indicates that a CLI command has finished with return code different from zero. diff --git a/robottelo/hosts.py b/robottelo/hosts.py index 2ff72bbe367..ba8f13add35 100644 --- a/robottelo/hosts.py +++ b/robottelo/hosts.py @@ -1599,6 +1599,21 @@ def get_features(self): """Get capsule features""" return requests.get(f'https://{self.hostname}:9090/features', verify=False).text + def enable_capsule_downstream_repos(self): + """Enabling CDN repos and capsule downstream repos on Capsule Host""" + # CDN Repos + self.register_to_cdn() + for repo in getattr(constants, f"OHSNAP_RHEL{self.os_version.major}_REPOS"): + result = self.enable_repo(repo, force=True) + if result.status: + raise CapsuleHostError(f'Repo enable at capsule host failed\n{result.stdout}') + # Downstream Capsule specific Repos + self.download_repofile( + product='capsule', + release=settings.capsule.version.release, + snap=settings.capsule.version.snap, + ) + def capsule_setup(self, sat_host=None, capsule_cert_opts=None, **installer_kwargs): """Prepare the host and run the capsule installer""" self._satellite = sat_host or Satellite() @@ -1686,7 +1701,10 @@ def run_installer_arg(self, *args, timeout='20m'): timeout=timeout, ) if result.status != 0: - raise SatelliteHostError(f'Failed to execute with argument: {result.stderr}') + raise SatelliteHostError( + f'Failed to execute with arguments: {installer_args} and,' + f' the stderr is {result.stderr}' + ) def set_mqtt_resend_interval(self, value): """Set the time interval in seconds at which the notification should be diff --git a/tests/foreman/api/test_capsule.py b/tests/foreman/api/test_capsule.py index 3d499ccf16e..8551f129376 100644 --- a/tests/foreman/api/test_capsule.py +++ b/tests/foreman/api/test_capsule.py @@ -21,7 +21,7 @@ @pytest.mark.e2e @pytest.mark.upgrade @pytest.mark.tier1 -def test_positive_update_capsule(target_sat, module_capsule_configured): +def test_positive_update_capsule(request, pytestconfig, target_sat, module_capsule_configured): """Update various capsule properties :id: a3d3eaa9-ed8d-42e6-9c83-20251e5ca9af @@ -39,7 +39,7 @@ def test_positive_update_capsule(target_sat, module_capsule_configured): :customerscenario: true """ - new_name = f'{gen_string("alpha")}-{module_capsule_configured.name}' + new_name = f'{gen_string("alpha")}-{module_capsule_configured.hostname}' capsule = target_sat.api.SmartProxy().search( query={'search': f'name = {module_capsule_configured.hostname}'} )[0] @@ -74,6 +74,16 @@ def test_positive_update_capsule(target_sat, module_capsule_configured): assert capsule.url in [cps.url for cps in capsules] assert capsule.name in [cps.name for cps in capsules] + @request.addfinalizer + def _finalize(): + if pytestconfig.option.n_minus: + cap = target_sat.api.SmartProxy().search(query={'search': f'name = {new_name}'}) + # Reinitializing the hostname + if cap: + cap = cap[0] + cap.name = module_capsule_configured.hostname + cap.update(['name']) + @pytest.mark.skip_if_not_set('fake_capsules') @pytest.mark.tier1 diff --git a/tests/foreman/api/test_capsulecontent.py b/tests/foreman/api/test_capsulecontent.py index abbfd9f9f32..7b666ff8d42 100644 --- a/tests/foreman/api/test_capsulecontent.py +++ b/tests/foreman/api/test_capsulecontent.py @@ -1263,6 +1263,7 @@ def test_positive_capsule_sync_status_persists( def test_positive_remove_capsule_orphans( self, target_sat, + pytestconfig, capsule_configured, function_entitlement_manifest_org, function_lce_library, @@ -1312,8 +1313,9 @@ def test_positive_remove_capsule_orphans( data={'environment_id': function_lce_library.id} ) result = capsule_configured.nailgun_capsule.content_lifecycle_environments() - assert len(result['results']) == 1 - assert result['results'][0]['id'] == function_lce_library.id + if not pytestconfig.option.n_minus: + assert len(result['results']) == 1 + assert result['results'][0]['id'] == function_lce_library.id sync_status = capsule_configured.nailgun_capsule.content_sync() assert sync_status['result'] == 'success', 'Capsule sync task failed.' diff --git a/tests/foreman/destructive/test_registration.py b/tests/foreman/destructive/test_registration.py index 357e779c5d7..9c8dbfa4929 100644 --- a/tests/foreman/destructive/test_registration.py +++ b/tests/foreman/destructive/test_registration.py @@ -14,6 +14,8 @@ from robottelo.config import settings +pytestmark = pytest.mark.destructive + @pytest.mark.tier3 @pytest.mark.no_containers