From 99bb065902b5fdbbeee0eda95478a2a2a4d6e2e7 Mon Sep 17 00:00:00 2001 From: Shubham Ganar <67952129+shubhamsg199@users.noreply.github.com> Date: Tue, 26 Dec 2023 18:21:53 +0530 Subject: [PATCH] Add Capsule Provisioning test (#13241) Signed-off-by: Shubham Ganar Signed-off-by: Gaurav Talreja (cherry picked from commit ff9a765adbf2ce5c0e9b2d69fc9f56d017ccd281) --- conftest.py | 1 + .../component/provision_capsule_pxe.py | 272 ++++++++++++++++++ pytest_plugins/fixture_markers.py | 1 + tests/foreman/api/test_provisioning.py | 125 ++++++++ 4 files changed, 399 insertions(+) create mode 100644 pytest_fixtures/component/provision_capsule_pxe.py diff --git a/conftest.py b/conftest.py index 1b4510ec7e4..4aefcff26b0 100644 --- a/conftest.py +++ b/conftest.py @@ -53,6 +53,7 @@ 'pytest_fixtures.component.provision_gce', 'pytest_fixtures.component.provision_libvirt', 'pytest_fixtures.component.provision_pxe', + 'pytest_fixtures.component.provision_capsule_pxe', 'pytest_fixtures.component.provision_vmware', 'pytest_fixtures.component.provisioning_template', 'pytest_fixtures.component.puppet', diff --git a/pytest_fixtures/component/provision_capsule_pxe.py b/pytest_fixtures/component/provision_capsule_pxe.py new file mode 100644 index 00000000000..4e3a3a2cf54 --- /dev/null +++ b/pytest_fixtures/component/provision_capsule_pxe.py @@ -0,0 +1,272 @@ +import ipaddress + +from box import Box +from broker import Broker +from fauxfactory import gen_string +from packaging.version import Version +import pytest + +from robottelo import constants +from robottelo.config import settings + + +@pytest.fixture(scope='module') +def capsule_provisioning_sat( + request, + module_target_sat, + module_sca_manifest_org, + module_location, + module_capsule_configured, +): + """ + This fixture sets up the Satellite for PXE provisioning. + It calls a workflow using broker to set up the network and to run satellite-installer. + It uses the artifacts from the workflow to create all the necessary Satellite entities + that are later used by the tests. + """ + # Assign org and loc + capsule = module_capsule_configured.nailgun_smart_proxy + capsule.location = [module_location] + capsule.update(['location']) + capsule.organization = [module_sca_manifest_org] + capsule.update(['organization']) + + provisioning_type = getattr(request, 'param', '') + sat = module_target_sat + provisioning_domain_name = f"{gen_string('alpha').lower()}.foo" + broker_data_out = Broker().execute( + workflow='configure-install-sat-provisioning-rhv', + artifacts='last', + target_vlan_id=settings.provisioning.vlan_id, + target_host=module_capsule_configured.name, + provisioning_dns_zone=provisioning_domain_name, + sat_version='stream' if sat.is_stream else sat.version, + deploy_scenario='capsule', + ) + + broker_data_out = Box(**broker_data_out['data_out']) + provisioning_interface = ipaddress.ip_interface(broker_data_out.provisioning_addr_ipv4) + provisioning_network = provisioning_interface.network + # TODO: investigate DNS setup issue on Satellite, + # we might need to set up Sat's DNS server as the primary one on the Sat host + provisioning_upstream_dns_primary = ( + broker_data_out.provisioning_upstream_dns.pop() + ) # There should always be at least one upstream DNS + provisioning_upstream_dns_secondary = ( + broker_data_out.provisioning_upstream_dns.pop() + if len(broker_data_out.provisioning_upstream_dns) + else None + ) + domain = sat.api.Domain( + location=[module_location], + organization=[module_sca_manifest_org], + dns=capsule.id, + name=provisioning_domain_name, + ).create() + + subnet = sat.api.Subnet( + location=[module_location], + organization=[module_sca_manifest_org], + network=str(provisioning_network.network_address), + mask=str(provisioning_network.netmask), + gateway=broker_data_out.provisioning_gw_ipv4, + from_=broker_data_out.provisioning_host_range_start, + to=broker_data_out.provisioning_host_range_end, + dns_primary=provisioning_upstream_dns_primary, + dns_secondary=provisioning_upstream_dns_secondary, + boot_mode='DHCP', + ipam='DHCP', + dhcp=capsule.id, + tftp=capsule.id, + template=capsule.id, + dns=capsule.id, + httpboot=capsule.id, + # discovery=capsule.id, + remote_execution_proxy=[capsule.id], + domain=[domain.id], + ).create() + + return Box( + sat=sat, + domain=domain, + subnet=subnet, + provisioning_type=provisioning_type, + broker_data=broker_data_out, + ) + + +@pytest.fixture(scope='module') +def capsule_provisioning_lce_sync_setup(module_capsule_configured, module_lce_library): + """This fixture adds the lifecycle environment to the capsule and syncs the content""" + module_capsule_configured.nailgun_capsule.content_add_lifecycle_environment( + data={'environment_id': module_lce_library.id} + ) + sync_status = module_capsule_configured.nailgun_capsule.content_sync(timeout=600) + assert sync_status['result'] == 'success', 'Capsule sync task failed.' + + +@pytest.fixture +def capsule_provisioning_hostgroup( + module_target_sat, + capsule_provisioning_sat, + module_sca_manifest_org, + module_location, + default_architecture, + capsule_provisioning_rhel_content, + module_lce_library, + default_partitiontable, + pxe_loader, + module_capsule_configured, +): + capsule = module_capsule_configured.nailgun_smart_proxy + provisioning_ip = capsule_provisioning_sat.broker_data.provisioning_addr_ipv4 + provisioning_ip = ipaddress.ip_interface(provisioning_ip).ip + return capsule_provisioning_sat.sat.api.HostGroup( + organization=[module_sca_manifest_org], + location=[module_location], + architecture=default_architecture, + domain=capsule_provisioning_sat.domain, + content_source=capsule.id, + content_view=capsule_provisioning_rhel_content.cv, + kickstart_repository=capsule_provisioning_rhel_content.ksrepo, + lifecycle_environment=module_lce_library, + root_pass=settings.provisioning.host_root_password, + operatingsystem=capsule_provisioning_rhel_content.os, + ptable=default_partitiontable, + subnet=capsule_provisioning_sat.subnet, + pxe_loader=pxe_loader.pxe_loader, + group_parameters_attributes=[ + { + 'name': 'remote_execution_ssh_keys', + 'parameter_type': 'string', + 'value': settings.provisioning.host_ssh_key_pub, + }, + # assign AK in order the hosts to be subscribed + { + 'name': 'kt_activation_keys', + 'parameter_type': 'string', + 'value': capsule_provisioning_rhel_content.ak.name, + }, + { + 'name': 'http_proxy', + 'parameter_type': 'string', + 'value': str(provisioning_ip), + }, + { + 'name': 'http_proxy_port', + 'parameter_type': 'string', + 'value': '80', + }, + ], + ).create() + + +@pytest.fixture(scope='module') +def capsule_provisioning_rhel_content( + request, + capsule_provisioning_sat, + module_sca_manifest_org, + module_lce_library, +): + """ + This fixture sets up kickstart repositories for a specific RHEL version + that is specified in `request.param`. + """ + sat = capsule_provisioning_sat.sat + rhel_ver = request.param + repo_names = [] + if int(rhel_ver) <= 7: + repo_names.append(f'rhel{rhel_ver}') + else: + repo_names.append(f'rhel{rhel_ver}_bos') + repo_names.append(f'rhel{rhel_ver}_aps') + rh_repos = [] + tasks = [] + rh_repo_id = "" + content_view = sat.api.ContentView(organization=module_sca_manifest_org).create() + + # Custom Content for Client repo + custom_product = sat.api.Product( + organization=module_sca_manifest_org, name=f'rhel{rhel_ver}_{gen_string("alpha")}' + ).create() + client_repo = sat.api.Repository( + organization=module_sca_manifest_org, + product=custom_product, + content_type='yum', + url=settings.repos.SATCLIENT_REPO[f'rhel{rhel_ver}'], + ).create() + task = client_repo.sync(synchronous=False) + tasks.append(task) + content_view.repository = [client_repo] + + for name in repo_names: + rh_kickstart_repo_id = sat.api_factory.enable_rhrepo_and_fetchid( + basearch=constants.DEFAULT_ARCHITECTURE, + org_id=module_sca_manifest_org.id, + product=constants.REPOS['kickstart'][name]['product'], + repo=constants.REPOS['kickstart'][name]['name'], + reposet=constants.REPOS['kickstart'][name]['reposet'], + releasever=constants.REPOS['kickstart'][name]['version'], + ) + # do not sync content repos for discovery based provisioning. + if not capsule_provisioning_sat.provisioning_type == 'discovery': + rh_repo_id = sat.api_factory.enable_rhrepo_and_fetchid( + basearch=constants.DEFAULT_ARCHITECTURE, + org_id=module_sca_manifest_org.id, + product=constants.REPOS[name]['product'], + repo=constants.REPOS[name]['name'], + reposet=constants.REPOS[name]['reposet'], + releasever=constants.REPOS[name]['releasever'], + ) + + # Sync step because repo is not synced by default + for repo_id in [rh_kickstart_repo_id, rh_repo_id]: + if repo_id: + rh_repo = sat.api.Repository(id=repo_id).read() + task = rh_repo.sync(synchronous=False) + tasks.append(task) + rh_repos.append(rh_repo) + content_view.repository.append(rh_repo) + content_view.update(['repository']) + for task in tasks: + sat.wait_for_tasks( + search_query=(f'id = {task["id"]}'), + poll_timeout=2500, + ) + task_status = sat.api.ForemanTask(id=task['id']).poll() + assert task_status['result'] == 'success' + rhel_xy = Version( + constants.REPOS['kickstart'][f'rhel{rhel_ver}']['version'] + if rhel_ver == 7 + else constants.REPOS['kickstart'][f'rhel{rhel_ver}_bos']['version'] + ) + o_systems = sat.api.OperatingSystem().search( + query={'search': f'family=Redhat and major={rhel_xy.major} and minor={rhel_xy.minor}'} + ) + assert o_systems, f'Operating system RHEL {rhel_xy} was not found' + os = o_systems[0].read() + # return only the first kickstart repo - RHEL X KS or RHEL X BaseOS KS + ksrepo = rh_repos[0] + publish = content_view.publish() + task_status = sat.wait_for_tasks( + search_query=(f'Actions::Katello::ContentView::Publish and id = {publish["id"]}'), + search_rate=15, + max_tries=10, + ) + assert task_status[0].result == 'success' + content_view = sat.api.ContentView( + organization=module_sca_manifest_org, name=content_view.name + ).search()[0] + ak = sat.api.ActivationKey( + organization=module_sca_manifest_org, + content_view=content_view, + environment=module_lce_library, + ).create() + + # Ensure client repo is enabled in the activation key + content = ak.product_content(data={'content_access_mode_all': '1'})['results'] + client_repo_label = [repo['label'] for repo in content if repo['name'] == client_repo.name][0] + ak.content_override( + data={'content_overrides': [{'content_label': client_repo_label, 'value': '1'}]} + ) + return Box(os=os, ak=ak, ksrepo=ksrepo, cv=content_view) diff --git a/pytest_plugins/fixture_markers.py b/pytest_plugins/fixture_markers.py index 888a720d13e..41e12b85bab 100644 --- a/pytest_plugins/fixture_markers.py +++ b/pytest_plugins/fixture_markers.py @@ -7,6 +7,7 @@ 'rhel_contenthost', 'content_hosts', 'module_provisioning_rhel_content', + 'capsule_provisioning_rhel_content', 'rex_contenthost', ] diff --git a/tests/foreman/api/test_provisioning.py b/tests/foreman/api/test_provisioning.py index b2cb3388b3b..cee77929db8 100644 --- a/tests/foreman/api/test_provisioning.py +++ b/tests/foreman/api/test_provisioning.py @@ -585,6 +585,131 @@ def test_rhel_pxe_provisioning_fips_enabled( assert provisioning_host.subscribed, 'Host is not subscribed' +@pytest.mark.e2e +@pytest.mark.parametrize('pxe_loader', ['bios', 'uefi'], indirect=True) +@pytest.mark.rhel_ver_match('[^6]') +def test_capsule_pxe_provisioning( + request, + capsule_provisioning_sat, + module_capsule_configured, + capsule_provisioning_rhel_content, + module_sca_manifest_org, + module_location, + provisioning_host, + pxe_loader, + capsule_provisioning_hostgroup, + module_lce_library, + module_default_org_view, + capsule_provisioning_lce_sync_setup, +): + """Provision a host using external capsule + + :id: d76cd326-af4e-4bd5-b20c-128348e042d3 + + :steps: + 1. Configure satellite and capsule for provisioning + 2. Provision a host using capsule as the content source + 3. Check that resulting host is registered to Satellite + + :expectedresults: + 1. Provisioning using external capsule is successful. + 1. Host installs right version of RHEL + 2. Satellite is able to run REX job on the host + 3. Host is registered to Satellite and subscription status is 'Success' + + :parametrized: yes + """ + host_mac_addr = provisioning_host._broker_args['provisioning_nic_mac_addr'] + sat = capsule_provisioning_sat.sat + cap = module_capsule_configured + host = sat.api.Host( + hostgroup=capsule_provisioning_hostgroup, + organization=module_sca_manifest_org, + location=module_location, + content_facet_attributes={ + 'content_view_id': capsule_provisioning_rhel_content.cv.id, + 'lifecycle_environment_id': module_lce_library.id, + }, + name=gen_string('alpha').lower(), + mac=host_mac_addr, + operatingsystem=capsule_provisioning_rhel_content.os, + subnet=capsule_provisioning_sat.subnet, + host_parameters_attributes=[ + { + 'name': 'remote_execution_connect_by_ip', + 'value': 'true', + 'parameter_type': 'boolean', + }, + ], + build=True, # put the host in build mode + ).create(create_missing=False) + # Clean up the host to free IP leases on Satellite. + # broker should do that as a part of the teardown, putting here just to make sure. + request.addfinalizer(host.delete) + # Start the VM, do not ensure that we can connect to SSHD + provisioning_host.power_control(ensure=False) + # Host should do call back to the Satellite reporting + # the result of the installation. Wait until Satellite reports that the host is installed. + wait_for( + lambda: host.read().build_status_label != 'Pending installation', + timeout=1500, + delay=10, + ) + host = host.read() + assert host.build_status_label == 'Installed' + + # Change the hostname of the host as we know it already. + # In the current infra environment we do not support + # addressing hosts using FQDNs, falling back to IP. + provisioning_host.hostname = host.ip + # Host is not blank anymore + provisioning_host.blank = False + + # Wait for the host to be rebooted and SSH daemon to be started. + provisioning_host.wait_for_connection() + + # Perform version check and check if root password is properly updated + host_os = host.operatingsystem.read() + expected_rhel_version = f'{host_os.major}.{host_os.minor}' + + if int(host_os.major) >= 9: + assert ( + provisioning_host.execute( + 'echo -e "\nPermitRootLogin yes" >> /etc/ssh/sshd_config; systemctl restart sshd' + ).status + == 0 + ) + host_ssh_os = sat.execute( + f'sshpass -p {settings.provisioning.host_root_password} ' + 'ssh -o StrictHostKeyChecking=no -o PubkeyAuthentication=no -o PasswordAuthentication=yes ' + f'-o UserKnownHostsFile=/dev/null root@{provisioning_host.hostname} cat /etc/redhat-release' + ) + assert host_ssh_os.status == 0 + assert ( + expected_rhel_version in host_ssh_os.stdout + ), f'The installed OS version differs from the expected version {expected_rhel_version}' + + # Run a command on the host using REX to verify that Satellite's SSH key is present on the host + template_id = ( + sat.api.JobTemplate().search(query={'search': 'name="Run Command - Script Default"'})[0].id + ) + job = sat.api.JobInvocation().run( + data={ + 'job_template_id': template_id, + 'inputs': { + 'command': f'subscription-manager config | grep "hostname = {cap.hostname}"' + }, + 'search_query': f"name = {host.name}", + 'targeting_type': 'static_query', + }, + ) + assert job['result'] == 'success', 'Job invocation failed' + + # assert that the host is subscribed and consumes + # subsctiption provided by the activation key + assert provisioning_host.subscribed, 'Host is not subscribed' + + @pytest.mark.stubbed def test_rhel_provisioning_using_realm(): """Provision a host using realm