diff --git a/robottelo/host_helpers/api_factory.py b/robottelo/host_helpers/api_factory.py index 6bd9cd8365c..672de88a32d 100644 --- a/robottelo/host_helpers/api_factory.py +++ b/robottelo/host_helpers/api_factory.py @@ -6,11 +6,13 @@ from contextlib import contextmanager from datetime import datetime import time +from unittest.mock import MagicMock from fauxfactory import gen_ipaddr, gen_mac, gen_string from nailgun import entity_mixins from nailgun.client import request from nailgun.entity_mixins import call_entity_method_with_timeout +import pytest from requests import HTTPError from robottelo.config import settings @@ -711,6 +713,213 @@ def wait_for_errata_applicability_task( f'No task was found using query " {search_query} " for host id: {host_id}' ) + def register_host_and_needed_setup( + self, + client, + organization, + activation_key, + environment, + content_view, + enable_repos=False, + rex_key=False, + force=False, + ): + """ + Method will setup desired entities to host content. Then, register the + host client to the entities, using associated activation-key. + + Attempt to make needed associations between detached entities. + + Add desired repos to the content-view prior to calling this helper. + Or, add them to content-view after calling, then publish/promote. + + param satellite: sat instance to create/associate needed entities. + param client: instance of a rhel contenthost. + param enable_repos (bool): enable all available repos on the client after registration? + default: False, be sure to enable repos after calling this method. + param rex_key (bool): add a Remote Execution Key to client for satellite? + default: False + param force (bool): force registration of the client to bypass? + default: False + + type: Below arguments can be any of the following: + int: pass id of entity to be read + str: pass name of entity to be searched + entity: pass an entity instance + + param organization: pass an Organization instance, name, or id to use. + param activation_key: pass an Activation-Key instance, name, or id. + param environment: pass a Lifecycle-Environment instance, name, or id. + for example: can pass string name 'Library'. + param content_view: pass a Content-View instance, name, or id. + for example: can pass string name 'Default Organization View'. + + Notes: + 1. Will fail if passed entities do not exist in the same organization. + 2. You can use param enable_repos, to try enabling any repositories added to passed content-view for client, + but if there are no available repositories from content-view, it will fail. + 3. The Default Organization View cannot be published, promoted, edited, or deleted. + but you can register the client to it. + + Steps: + 1. get needed entities from arguments, id, name, or instance. Read all as instance. + 2. publish the content-view if no versions exist, or needs_publish. + 3. promote the newest content-view-version if not in environment already. Skip for 'Library'. + 4. assign environment and content-view to the activation-key, if not associated. + 5. Register the host, using the activation-key associated with content. + + return: dict containing the updated entities: + { + 'client', 'organization', 'activation_key', + 'environment', 'content_view', + } + + """ + entities = { + 'Organization': organization, + 'ActivationKey': activation_key, + 'LifecycleEnvironment': environment, + 'ContentView': content_view, + } + assert hasattr(client, 'hostname') + + # method for updating entities in params dict above, + # called after entity modifications. + def read_entities(): + nonlocal entities + entities = {k: v.read() for k, v in entities.items()} + + # for entity arguments matched to above params: + # fetch entity instance on satellite + # from given id or name, else read passed argument as an instance. + for entity, value in entities.items(): + param = None + # passed int for entity, try to read by id + if isinstance(value, int): + param = getattr(self._satellite.api, entity)(id=value).read() + # passed str, search for entity by name + elif isinstance(value, str): + # search for org name itself, will be just scoped to satellite + if entity == 'Organization': + search_query = f'name="{value}"' + result = getattr(self._satellite.api, entity)().search( + query={'search': search_query} + ) + # search of non-org entity by name, will be scoped to organization + else: + search_query = ( + f"name='{value}' and organization_id={entities['Organization'].id}" + ) + result = getattr(self._satellite.api, entity)( + organization=entities['Organization'] + ).search(query={'search': search_query}) + if not len(result) > 0: + pytest.fail( + f'Could not find {entity} name: {value}, by search query: "{search_query}"' + ) + param = result[0] + # did not pass int (id) or str (name), must be readable entity instance + else: + if not hasattr(value, 'id'): + pytest.fail(f'Passed entity {entity}, has no attribute id:\n{value}') + param = value + # updated param, should now be only an entity isntance + assert hasattr(param, 'id'), ( + f'Did not get readable instance from parameter on {self._satellite.hostname}:' + f' Param:{entity}:\n{value}' + ) + # entity found, read updated instance into dictionary + entities[entity] = param.read() + + if ( # publish a content-view-version if none exist, or needs_publish is True + len(entities['ContentView'].version) == 0 + or entities['ContentView'].needs_publish is True + ): + entities['ContentView'].publish() + read_entities() + + # promote to non-Library env if not already present: + # skip for 'Library' env selected or passed arg, + # any published version(s) will already be in Library. + if all( + [ + environment != 'Library', + entities['LifecycleEnvironment'].name != 'Library', + entities['LifecycleEnvironment'] not in entities['ContentView'].environment, + ] + ): + # promote newest version by id + entities['ContentView'].version.sort(key=lambda version: version.id) + entities['ContentView'].version[-1].promote( + data={'environment_ids': entities['LifecycleEnvironment'].id} + ) + read_entities() + + if ( # assign env to ak if not present + entities['ActivationKey'].environment is None + or entities['ActivationKey'].environment.id != entities['LifecycleEnvironment'].id + ): + entities['ActivationKey'].environment = entities['LifecycleEnvironment'] + entities['ActivationKey'].update(['environment']) + read_entities() + if ( # assign cv to ak if not present + entities['ActivationKey'].content_view is None + or entities['ActivationKey'].content_view.id != entities['ContentView'].id + ): + entities['ActivationKey'].content_view = entities['ContentView'] + entities['ActivationKey'].update(['content_view']) + + # register with now setup entities, using ak + read_entities() + result = client.register( + activation_keys=entities['ActivationKey'].name, + target=self._satellite, + org=entities['Organization'], + loc=None, + force=force, + ) + if result.status != 0: + pytest.fail(f'Failed to register the host: {client.hostname}.\n{result.stderr}') + if not client.subscribed: + pytest.fail( + f"Failed to subscribe the host: {client.hostname}, to content-view: {entities['ContentView'].name}" + ) + if rex_key: + client.add_rex_key(self._satellite) + # attempt to enable all repositories available to client, + # ie any repos added to content-view prior to calling this method. + # note: this will fail if no repos are available to client from CV/AK + if enable_repos: + output = client.execute(r'subscription-manager repos --enable \*') + assert output.status == 0, ( + 'Failed to enable all available repositories using subscription-manager.' + f' For client: {client.hostname}.\n{output.stdout}' + ) + + # For in-between parameterized sessions: + # unregister the host if it's still subscribed. + request = MagicMock() + + @request.addfinalizer + def cleanup(): + nonlocal client + if client.subscribed: + client.unregister() + assert ( + client.subscribed is False + ), f'Failed to fully teardown client {client.hostname}, maintains some content association.' + + read_entities() + return ( # dict containing registered host client, and updated entities + { + 'client': client, + 'organization': entities['Organization'], + 'activation_key': entities['ActivationKey'], + 'environment': entities['LifecycleEnvironment'], + 'content_view': entities['ContentView'], + } + ) + def wait_for_syncplan_tasks(self, repo_backend_id=None, timeout=10, repo_name=None): """Search the pulp tasks and identify repositories sync tasks with specified name or backend_identifier diff --git a/tests/foreman/ui/test_errata.py b/tests/foreman/ui/test_errata.py index ba5c9b27ab9..303179a540f 100644 --- a/tests/foreman/ui/test_errata.py +++ b/tests/foreman/ui/test_errata.py @@ -353,12 +353,14 @@ def cleanup(): @pytest.mark.rhel_ver_match('[^6]') @pytest.mark.no_containers def test_end_to_end( - session, - module_org, + module_sca_manifest_org, + module_target_sat, + rhel_contenthost, + module_product, module_lce, + module_ak, module_cv, - module_target_sat, - registered_contenthost, + session, ): """Create all entities required for errata, set up applicable host, read errata details and apply it to host. @@ -386,6 +388,8 @@ def test_end_to_end( 'reboot_suggested': 'No', 'topic': '', 'description': 'Sea_Erratum', + 'issued': '2012-01-27', + 'last_updated_on': '2012-01-27', 'solution': '', } ERRATA_PACKAGES = { @@ -396,63 +400,104 @@ def test_end_to_end( ], 'module_stream_packages': [], } - _UTC_format = '%Y-%m-%d %H:%M:%S UTC' - # Capture newest product and repository with the desired content - product_list = module_target_sat.api.Product(organization=module_org).search() - assert len(product_list) > 0 - product_list.sort(key=lambda product: product.id) - _product = product_list[-1].read() - assert len(_product.repository) == 1 - _repository = _product.repository[0].read() - # Remove custom package if present, install outdated version - registered_contenthost.execute(f'yum remove -y {FAKE_1_CUSTOM_PACKAGE_NAME}') - result = registered_contenthost.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') - assert result.status == 0, f'Failed to install package {FAKE_1_CUSTOM_PACKAGE}.' - # recalculate and assert applicable errata after installing outdated pkg - assert registered_contenthost.execute('subscription-manager repos').status == 0 - applicable_errata = registered_contenthost.applicable_errata_count + # create custom repo, sync, add to content view + custom_repo = module_target_sat.api.Repository( + url=CUSTOM_REPO_URL, product=module_product + ).create() + custom_repo.sync() + module_cv.repository = [custom_repo] + module_cv.update(['repository']) + module_cv = module_cv.read() + + # associate needed entities, prepare content view, register the host client: + # we will also enable the custom repo we added to module_cv + client = module_target_sat.api_factory.register_host_and_needed_setup( + organization=module_sca_manifest_org, + client=rhel_contenthost, + activation_key=module_ak, + environment=module_lce, + content_view=module_cv, + enable_repos=True, + )['client'] + + # nothing applicable to start + result = client.execute('subscription-manager repos') + assert ( + result.status == 0 + ), f'Failure invoking subscription-manager, for client: {client.hostname}.\n{result.stdout}' + assert ( + 0 == client.applicable_errata_count == client.applicable_package_count + ), f'Expected no applicable erratum or packages to start, on host: {client.hostname}' + # install outdated package version, making an errata applicable + result = client.execute(f'yum install -y {FAKE_1_CUSTOM_PACKAGE}') + assert ( + result.status == 0 + ), f'Failed to install package {FAKE_1_CUSTOM_PACKAGE}.\n{result.stdout}' + # recalculate and assert app errata, after installing outdated pkg + assert client.execute('subscription-manager repos').status == 0 + applicable_errata = client.applicable_errata_count assert ( applicable_errata == 1 ), f'Expected 1 applicable errata: {CUSTOM_REPO_ERRATA_ID}, after setup. Got {applicable_errata}' - with session: datetime_utc_start = datetime.utcnow() # Check selection box function for BZ#1688636 session.location.select(loc_name=DEFAULT_LOC) - session.organization.select(org_name=module_org.name) results = session.errata.search_content_hosts( entity_name=CUSTOM_REPO_ERRATA_ID, - value=registered_contenthost.hostname, + value=client.hostname, environment=module_lce.name, ) assert len(results) == 1 - assert results[0]['Name'] == registered_contenthost.hostname + assert results[0]['Name'] == client.hostname errata = session.errata.read(CUSTOM_REPO_ERRATA_ID) - assert errata['repositories']['table'][-1]['Name'] == _repository.name - assert errata['repositories']['table'][-1]['Product'] == _product.name + assert errata['repositories']['table'], ( + f'There are no repositories listed for errata ({CUSTOM_REPO_ERRATA_ID}),', + f' expected to find at least one repository, name: {custom_repo.name}.', + ) + # repo/product entry in table match expected + # find the first table entry with the custom repository's name + errata_repo = next( + ( + repo + for repo in errata['repositories']['table'] + if 'Name' in repo and repo['Name'] == custom_repo.name + ), + None, + ) + # assert custom repo found and product name + assert ( + errata_repo + ), f'Could not find the errata repository in UI by name: {custom_repo.name}.' + assert errata_repo['Name'] == custom_repo.name + assert ( + errata_repo['Product'] == module_product.name + ), 'The product name for the errata repository in UI does not match.' # Check all tabs of Errata Details page assert ( not ERRATA_DETAILS.items() - errata['details'].items() ), 'Errata details do not match expected values.' - assert parse(errata['details']['issued']) == parse('2012-01-27 12:00:00 AM') - assert parse(errata['details']['last_updated_on']) == parse('2012-01-27 12:00:00 AM') + assert parse(errata['details']['issued']) == parse( + ERRATA_DETAILS['issued'] + ), 'Errata issued date in UI does not match.' + assert parse(errata['details']['last_updated_on']) == parse( + ERRATA_DETAILS['last_updated_on'] + ), 'Errata last updated date in UI does not match.' assert set(errata['packages']['independent_packages']) == set( ERRATA_PACKAGES['independent_packages'] - ) + ), 'Set of errata packages in UI does not match.' assert ( errata['packages']['module_stream_packages'] == ERRATA_PACKAGES['module_stream_packages'] - ) - + ), 'Errata module streams in UI does not match.' # Apply Errata, find REX install task session.host_new.apply_erratas( - entity_name=registered_contenthost.hostname, + entity_name=client.hostname, search=f"errata_id == {CUSTOM_REPO_ERRATA_ID}", ) results = module_target_sat.wait_for_tasks( search_query=( - f'"Install errata errata_id == {CUSTOM_REPO_ERRATA_ID}' - f' on {registered_contenthost.hostname}"' + f'"Install errata errata_id == {CUSTOM_REPO_ERRATA_ID}' f' on {client.hostname}"' ), search_rate=2, max_tries=60, @@ -464,18 +509,19 @@ def test_end_to_end( task_status['result'] == 'success' ), f'Errata Installation task failed:\n{task_status}' assert ( - registered_contenthost.applicable_errata_count == 0 - ), 'Unexpected applicable errata found after install.' + client.applicable_errata_count == 0 + ), f'Unexpected applicable errata found after install of {CUSTOM_REPO_ERRATA_ID}.' # UTC timing for install task and session + _UTC_format = '%Y-%m-%d %H:%M:%S UTC' install_start = datetime.strptime(task_status['started_at'], _UTC_format) install_end = datetime.strptime(task_status['ended_at'], _UTC_format) + # install task duration did not exceed 1 minute, + # duration since start of session did not exceed 10 minutes. assert (install_end - install_start).total_seconds() <= 60 assert (install_end - datetime_utc_start).total_seconds() <= 600 # Find bulk generate applicability task results = module_target_sat.wait_for_tasks( - search_query=( - f'Bulk generate applicability for host {registered_contenthost.hostname}' - ), + search_query=(f'Bulk generate applicability for host {client.hostname}'), search_rate=2, max_tries=60, ) @@ -494,14 +540,12 @@ def test_end_to_end( assert session.errata.read(CUSTOM_REPO_ERRATA_ID) results = session.errata.search_content_hosts( entity_name=CUSTOM_REPO_ERRATA_ID, - value=registered_contenthost.hostname, + value=client.hostname, environment=module_lce.name, ) assert len(results) == 0 # Check package version was updated on contenthost - _package_version = registered_contenthost.execute( - f'rpm -q {FAKE_1_CUSTOM_PACKAGE_NAME}' - ).stdout + _package_version = client.execute(f'rpm -q {FAKE_1_CUSTOM_PACKAGE_NAME}').stdout assert FAKE_2_CUSTOM_PACKAGE in _package_version